From eac90d22f65af9629462ab53890fe2c19cb8b555 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Fri, 15 May 2020 15:13:05 +0200 Subject: [PATCH 01/69] Moved the isArrowKeyCode() helper to keyboard utils. --- packages/ckeditor5-table/src/tablenavigation.js | 17 ++++------------- packages/ckeditor5-utils/src/keyboard.js | 13 +++++++++++++ packages/ckeditor5-widget/src/widget.js | 16 ++++------------ 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/packages/ckeditor5-table/src/tablenavigation.js b/packages/ckeditor5-table/src/tablenavigation.js index 44ec32dc1cf..e328eecb4e4 100644 --- a/packages/ckeditor5-table/src/tablenavigation.js +++ b/packages/ckeditor5-table/src/tablenavigation.js @@ -15,7 +15,10 @@ import { getSelectedTableCells, getTableCellsContainingSelection } from './utils import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect'; import priorities from '@ckeditor/ckeditor5-utils/src/priorities'; -import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; +import { + keyCodes, + isArrowKeyCode +} from '@ckeditor/ckeditor5-utils/src/keyboard'; /** * This plugin enables keyboard navigation for tables. @@ -515,18 +518,6 @@ export default class TableNavigation extends Plugin { } } -// Returns `true` if the provided key code represents one of the arrow keys. -// -// @private -// @param {Number} keyCode -// @returns {Boolean} -function isArrowKeyCode( keyCode ) { - return keyCode == keyCodes.arrowright || - keyCode == keyCodes.arrowleft || - keyCode == keyCodes.arrowup || - keyCode == keyCodes.arrowdown; -} - // Returns the direction name from `keyCode`. // // @private diff --git a/packages/ckeditor5-utils/src/keyboard.js b/packages/ckeditor5-utils/src/keyboard.js index 61cb393f469..d5f6e6dfe82 100644 --- a/packages/ckeditor5-utils/src/keyboard.js +++ b/packages/ckeditor5-utils/src/keyboard.js @@ -129,6 +129,19 @@ export function getEnvKeystrokeText( keystroke ) { } ); } +/** + * Returns `true` if the provided key code represents one of the arrow keys. + * + * @param {Number} keyCode + * @returns {Boolean} + */ +export function isArrowKeyCode( keyCode ) { + return keyCode == keyCodes.arrowright || + keyCode == keyCodes.arrowleft || + keyCode == keyCodes.arrowup || + keyCode == keyCodes.arrowdown; +} + function generateKnownKeyCodes() { const keyCodes = { arrowleft: 37, diff --git a/packages/ckeditor5-widget/src/widget.js b/packages/ckeditor5-widget/src/widget.js index bdbfd6850d7..f4279787251 100644 --- a/packages/ckeditor5-widget/src/widget.js +++ b/packages/ckeditor5-widget/src/widget.js @@ -11,7 +11,10 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobserver'; import WidgetTypeAround from './widgettypearound/widgettypearound'; import { getLabel, isWidget, WIDGET_SELECTED_CLASS_NAME } from './utils'; -import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; +import { + keyCodes, + isArrowKeyCode +} from '@ckeditor/ckeditor5-utils/src/keyboard'; import env from '@ckeditor/ckeditor5-utils/src/env'; import '../theme/widget.css'; @@ -364,17 +367,6 @@ export default class Widget extends Plugin { } } -// Returns 'true' if provided key code represents one of the arrow keys. -// -// @param {Number} keyCode -// @returns {Boolean} -function isArrowKeyCode( keyCode ) { - return keyCode == keyCodes.arrowright || - keyCode == keyCodes.arrowleft || - keyCode == keyCodes.arrowup || - keyCode == keyCodes.arrowdown; -} - // Returns `true` when element is a nested editable or is placed inside one. // // @param {module:engine/view/element~Element} From 0c757a151bf520042423b1993745859dd116bc2e Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Fri, 15 May 2020 16:17:59 +0200 Subject: [PATCH 02/69] =?UTF-8?q?Extracted=20more=20arrow=20keys=E2=80=93r?= =?UTF-8?q?elated=20utils=20to=20ckeditor5-utils.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ckeditor5-table/src/tablenavigation.js | 30 ++---------- packages/ckeditor5-utils/src/keyboard.js | 48 +++++++++++++++++++ packages/ckeditor5-widget/src/widget.js | 7 +-- 3 files changed, 56 insertions(+), 29 deletions(-) diff --git a/packages/ckeditor5-table/src/tablenavigation.js b/packages/ckeditor5-table/src/tablenavigation.js index e328eecb4e4..bb724aa477e 100644 --- a/packages/ckeditor5-table/src/tablenavigation.js +++ b/packages/ckeditor5-table/src/tablenavigation.js @@ -16,8 +16,8 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect'; import priorities from '@ckeditor/ckeditor5-utils/src/priorities'; import { - keyCodes, - isArrowKeyCode + isArrowKeyCode, + getLocalizedArrowKeyCodeDirection } from '@ckeditor/ckeditor5-utils/src/keyboard'; /** @@ -166,13 +166,14 @@ export default class TableNavigation extends Plugin { * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData */ _onKeydown( eventInfo, domEventData ) { + const editor = this.editor; const keyCode = domEventData.keyCode; if ( !isArrowKeyCode( keyCode ) ) { return; } - const direction = getDirectionFromKeyCode( keyCode, this.editor.locale.contentLanguageDirection ); + const direction = getLocalizedArrowKeyCodeDirection( keyCode, editor.locale.contentLanguageDirection ); const wasHandled = this._handleArrowKeys( direction, domEventData.shiftKey ); if ( wasHandled ) { @@ -518,26 +519,3 @@ export default class TableNavigation extends Plugin { } } -// Returns the direction name from `keyCode`. -// -// @private -// @param {Number} keyCode -// @param {String} contentLanguageDirection The content language direction. -// @returns {'left'|'up'|'right'|'down'} Arrow direction. -function getDirectionFromKeyCode( keyCode, contentLanguageDirection ) { - const isLtrContent = contentLanguageDirection === 'ltr'; - - switch ( keyCode ) { - case keyCodes.arrowleft: - return isLtrContent ? 'left' : 'right'; - - case keyCodes.arrowright: - return isLtrContent ? 'right' : 'left'; - - case keyCodes.arrowup: - return 'up'; - - case keyCodes.arrowdown: - return 'down'; - } -} diff --git a/packages/ckeditor5-utils/src/keyboard.js b/packages/ckeditor5-utils/src/keyboard.js index d5f6e6dfe82..542de2ad631 100644 --- a/packages/ckeditor5-utils/src/keyboard.js +++ b/packages/ckeditor5-utils/src/keyboard.js @@ -142,6 +142,54 @@ export function isArrowKeyCode( keyCode ) { keyCode == keyCodes.arrowdown; } +/** + * Returns the direction from the provided key code considering the language direction of the + * editor content. + * + * For instance, in right–to–left (RTL) languages, pressing the left arrow means moving selection right (forward) + * in the model structure. Similarly, pressing the right arrow moves the selection left (backward). + * + * @param {Number} keyCode + * @param {String} contentLanguageDirection The content language direction, corresponding to + * {@link module:utils/locale~Locale#contentLanguageDirection}. + * @returns {'left'|'up'|'right'|'down'} Arrow direction. + */ +export function getLocalizedArrowKeyCodeDirection( keyCode, contentLanguageDirection ) { + const isLtrContent = contentLanguageDirection === 'ltr'; + + switch ( keyCode ) { + case keyCodes.arrowleft: + return isLtrContent ? 'left' : 'right'; + + case keyCodes.arrowright: + return isLtrContent ? 'right' : 'left'; + + case keyCodes.arrowup: + return 'up'; + + case keyCodes.arrowdown: + return 'down'; + } +} + +/** + * Determines if the provided key code moves the selection forward or backward considering + * the language direction of the editor content. + * + * For instance, in right–to–left (RTL) languages, pressing the left arrow means moving forward + * in the model structure. Similarly, pressing the right arrow moves the selection backward. + * + * @param {Number} keyCode + * @param {String} contentLanguageDirection The content language direction, corresponding to + * {@link module:utils/locale~Locale#contentLanguageDirection}. + * @returns {Boolean} + */ +export function isForwardArrowKeyCode( keyCode, contentLanguageDirection ) { + const localizedKeyCodeDirection = getLocalizedArrowKeyCodeDirection( keyCode, contentLanguageDirection ); + + return localizedKeyCodeDirection === 'down' || localizedKeyCodeDirection === 'right'; +} + function generateKnownKeyCodes() { const keyCodes = { arrowleft: 37, diff --git a/packages/ckeditor5-widget/src/widget.js b/packages/ckeditor5-widget/src/widget.js index f4279787251..2c0517eb655 100644 --- a/packages/ckeditor5-widget/src/widget.js +++ b/packages/ckeditor5-widget/src/widget.js @@ -13,7 +13,8 @@ import WidgetTypeAround from './widgettypearound/widgettypearound'; import { getLabel, isWidget, WIDGET_SELECTED_CLASS_NAME } from './utils'; import { keyCodes, - isArrowKeyCode + isArrowKeyCode, + isForwardArrowKeyCode } from '@ckeditor/ckeditor5-utils/src/keyboard'; import env from '@ckeditor/ckeditor5-utils/src/env'; @@ -173,13 +174,13 @@ export default class Widget extends Plugin { */ _onKeydown( eventInfo, domEventData ) { const keyCode = domEventData.keyCode; - const isLtrContent = this.editor.locale.contentLanguageDirection === 'ltr'; - const isForward = keyCode == keyCodes.arrowdown || keyCode == keyCodes[ isLtrContent ? 'arrowright' : 'arrowleft' ]; let wasHandled = false; // Checks if the keys were handled and then prevents the default event behaviour and stops // the propagation. if ( isArrowKeyCode( keyCode ) ) { + const isForward = isForwardArrowKeyCode( keyCode, this.editor.locale.contentLanguageDirection ); + wasHandled = this._handleArrowKeys( isForward ); } else if ( keyCode === keyCodes.enter ) { wasHandled = this._handleEnterKey( domEventData.shiftKey ); From 04ce6332dffdfa56842e3798b466e58c24be6b6b Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Fri, 15 May 2020 17:05:09 +0200 Subject: [PATCH 03/69] (WIP) PoC of the keyboard support. --- .../ckeditor5-widget/widgettypearound.css | 32 +++++ .../src/widgettypearound/widgettypearound.js | 126 ++++++++++++++++++ .../theme/widgettypearound.css | 23 ++++ 3 files changed, 181 insertions(+) diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css index 2ba7ec69570..6b78ba9c85b 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css @@ -101,6 +101,20 @@ &.ck-widget_with-selection-handle > .ck-widget__type-around > .ck-widget__type-around__button_before { margin-left: 20px; } + + & .ck-widget__type-around__line { + pointer-events: none; + height: var(--ck-widget-outline-thickness); + animation: ck-widget-type-around-line-pulse linear 1s infinite normal forwards; + } + + &.ck-widget_selected, + &.ck-widget.ck-widget_selected:hover { + &.ck-widget_type-around_active_before, + &.ck-widget_type-around_active_after { + outline-color: transparent; + } + } } /* @@ -143,3 +157,21 @@ box-shadow: 0 0 0 5px hsla(var(--ck-color-focus-border-coordinates), var(--ck-color-widget-type-around-button-radar-start-alpha)); } } + +@keyframes ck-widget-type-around-line-pulse { + 0% { + background: hsla(var(--ck-color-focus-border-coordinates), 0); + } + 40% { + background: hsla(var(--ck-color-focus-border-coordinates), 0); + } + 50% { + background: hsla(var(--ck-color-focus-border-coordinates), 1); + } + 90% { + background: hsla(var(--ck-color-focus-border-coordinates), 1); + } + 100% { + background: hsla(var(--ck-color-focus-border-coordinates), 0); + } +} diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 41583146c2d..95d755f26cb 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -12,6 +12,11 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Template from '@ckeditor/ckeditor5-ui/src/template'; +import { + isArrowKeyCode, + isForwardArrowKeyCode +} from '@ckeditor/ckeditor5-utils/src/keyboard'; +import priorities from '@ckeditor/ckeditor5-utils/src/priorities'; import { isTypeAroundWidget, @@ -29,6 +34,8 @@ const POSSIBLE_INSERTION_POSITIONS = [ 'before', 'after' ]; // Do the SVG parsing once and then clone the result DOM element for each new button. const RETURN_ARROW_ICON_ELEMENT = new DOMParser().parseFromString( returnIcon, 'image/svg+xml' ).firstChild; +const TYPE_AROUND_SELECTION_ATTRIBUTE = 'widget-type-around'; + /** * A plugin that allows users to type around widgets where normally it is impossible to place the caret due * to limitations of web browsers. These "tight spots" occur, for instance, before (or after) a widget being @@ -90,6 +97,7 @@ export default class WidgetTypeAround extends Plugin { this._enableTypeAroundUIInjection(); this._enableDetectionOfTypeAroundWidgets(); this._enableInsertingParagraphsOnButtonClick(); + this._enableTypeAroundActivationUsingKeyboardArrows(); // TODO: This is a quick fix and it should be removed the proper integration arrives. this._enableTemporaryTrackChangesIntegration(); @@ -243,6 +251,105 @@ export default class WidgetTypeAround extends Plugin { } ); } ); } + + /** + * @private + */ + _enableTypeAroundActivationUsingKeyboardArrows() { + const editor = this.editor; + const model = editor.model; + const modelSelection = model.document.selection; + const schema = model.schema; + const editingView = editor.editing.view; + + // Note: The priority must precede the default Widget class keydown handler. + editingView.document.on( 'keydown', ( evt, domEventData ) => { + const keyCode = domEventData.keyCode; + + if ( !isArrowKeyCode( keyCode ) ) { + return; + } + + const selectedViewElement = editingView.document.selection.getSelectedElement(); + const selectedModelElement = editor.editing.mapper.toModelElement( selectedViewElement ); + + if ( !isTypeAroundWidget( selectedViewElement, selectedModelElement, schema ) ) { + return; + } + + const isForward = isForwardArrowKeyCode( keyCode, editor.locale.contentLanguageDirection ); + const typeAroundSelectionAttribute = modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); + + editor.model.change( writer => { + let shouldStopAndPreventDefault; + + // If the selection already has the attribute... + if ( typeAroundSelectionAttribute ) { + const selectionPosition = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition(); + const nearestSelectionRange = schema.getNearestSelectionRange( selectionPosition, isForward ? 'forward' : 'backward' ); + const isLeavingWidget = typeAroundSelectionAttribute === ( isForward ? 'after' : 'before' ); + + // ...and the keyboard arrow matches the value of the selection attribute... + if ( isLeavingWidget ) { + // ...and if there is some place for the selection to go to... + if ( nearestSelectionRange ) { + // ...then just remove the attribute and let the default Widget plugin listener handle moving the selection. + writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); + } + + // If the selection had nowhere to go, let's leave the attribute as it was and pass through + // to the Widget plugin listener which will... in fact also do nothing. But this is no longer + // the problem of the WidgetTypeAround plugin. + } + // ...and the keyboard arrow works against the value of the selection attribute... + else { + // ...then remove the selection attribute but prevent default DOM actions + // and do not let the Widget plugin listener move the selection. This brings + // the widget back to the state, for instance, like if was selected using the mouse. + writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); + shouldStopAndPreventDefault = true; + } + } + // If the selection didn't have the attribute, let's set it now according to the direction of the arrow + // key press. This also means we cannot let the Widget plugin listener move the selection. + else { + writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, isForward ? 'after' : 'before' ); + shouldStopAndPreventDefault = true; + } + + if ( shouldStopAndPreventDefault ) { + domEventData.preventDefault(); + evt.stop(); + } + } ); + }, { priority: priorities.get( 'high' ) + 1 } ); + + modelSelection.on( 'change:range', () => { + // TODO clean the selection attribute? It's weird because it looks like it is cleared automatically. + } ); + + editor.editing.downcastDispatcher.on( 'selection', ( evt, data, conversionApi ) => { + const selectedModelElement = data.selection.getSelectedElement(); + const selectedViewElement = conversionApi.mapper.toViewElement( selectedModelElement ); + + if ( !isTypeAroundWidget( selectedViewElement, selectedModelElement, schema ) ) { + return; + } + + const typeAroundSelectionAttribute = data.selection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); + const writer = conversionApi.writer; + + if ( typeAroundSelectionAttribute ) { + writer.addClass( positionToWidgetCssClass( typeAroundSelectionAttribute ), selectedViewElement ); + } else { + writer.removeClass( POSSIBLE_INSERTION_POSITIONS.map( positionToWidgetCssClass ), selectedViewElement ); + } + } ); + + function positionToWidgetCssClass( position ) { + return `ck-widget_type-around_active_${ position }`; + } + } } // Injects the type around UI into a view widget instance. @@ -257,6 +364,7 @@ function injectUIIntoWidget( viewWriter, buttonTitles, widgetViewElement ) { const wrapperDomElement = this.toDomElement( domDocument ); injectButtons( wrapperDomElement, buttonTitles ); + injectLines( wrapperDomElement ); return wrapperDomElement; } ); @@ -291,3 +399,21 @@ function injectButtons( wrapperDomElement, buttonTitles ) { wrapperDomElement.appendChild( buttonTemplate.render() ); } } + +// @param {HTMLElement} wrapperDomElement +function injectLines( wrapperDomElement ) { + for ( const position of POSSIBLE_INSERTION_POSITIONS ) { + const lineTemplate = new Template( { + tag: 'div', + attributes: { + class: [ + 'ck', + 'ck-widget__type-around__line', + `ck-widget__type-around__line_${ position }` + ] + } + } ); + + wrapperDomElement.appendChild( lineTemplate.render() ); + } +} diff --git a/packages/ckeditor5-widget/theme/widgettypearound.css b/packages/ckeditor5-widget/theme/widgettypearound.css index b4ab804428d..d55c511d5f6 100644 --- a/packages/ckeditor5-widget/theme/widgettypearound.css +++ b/packages/ckeditor5-widget/theme/widgettypearound.css @@ -74,6 +74,29 @@ z-index: calc(var(--ck-z-default) + 1); } } + + & .ck-widget__type-around__line { + display: none; + position: absolute; + left: 0; + right: 0; + + &.ck-widget__type-around__line_before { + top: calc(-1 * var(--ck-widget-outline-thickness)); + } + + &.ck-widget__type-around__line_after { + bottom: calc(-1 * var(--ck-widget-outline-thickness)); + } + } + + &.ck-widget_type-around_active_before > .ck-widget__type-around > .ck-widget__type-around__line_before { + display: block; + } + + &.ck-widget_type-around_active_after > .ck-widget__type-around > .ck-widget__type-around__line_after { + display: block; + } } /* From 6a148141670d722b505082207b2b5627374b973e Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 21 May 2020 13:42:14 +0200 Subject: [PATCH 04/69] TMP --- .../src/widgettypearound/widgettypearound.js | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 95d755f26cb..a8050398b09 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -313,38 +313,58 @@ export default class WidgetTypeAround extends Plugin { // If the selection didn't have the attribute, let's set it now according to the direction of the arrow // key press. This also means we cannot let the Widget plugin listener move the selection. else { - writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, isForward ? 'after' : 'before' ); - shouldStopAndPreventDefault = true; + const widgetTypeAroundPositions = getWidgetTypeAroundPositions( selectedViewElement ); + + // Set the selection attribute only if the keystroke direction matches the type around position + // of the widget. + if ( isForward && widgetTypeAroundPositions.includes( 'after' ) ) { + writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'after' ); + shouldStopAndPreventDefault = true; + } else if ( !isForward && widgetTypeAroundPositions.includes( 'before' ) ) { + writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' ); + shouldStopAndPreventDefault = true; + } } if ( shouldStopAndPreventDefault ) { domEventData.preventDefault(); + domEventData.stopPropagation(); evt.stop(); } } ); }, { priority: priorities.get( 'high' ) + 1 } ); modelSelection.on( 'change:range', () => { - // TODO clean the selection attribute? It's weird because it looks like it is cleared automatically. + const editor = this.editor; + const model = editor.model; + const modelSelection = model.document.selection; + + if ( !modelSelection.hasAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ) { + return; + } + + editor.model.change( writer => { + // TODO: use data.directChange to not break collaboration? + writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); + } ); } ); editor.editing.downcastDispatcher.on( 'selection', ( evt, data, conversionApi ) => { + const writer = conversionApi.writer; const selectedModelElement = data.selection.getSelectedElement(); const selectedViewElement = conversionApi.mapper.toViewElement( selectedModelElement ); + const typeAroundSelectionAttribute = data.selection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); if ( !isTypeAroundWidget( selectedViewElement, selectedModelElement, schema ) ) { return; } - const typeAroundSelectionAttribute = data.selection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); - const writer = conversionApi.writer; - if ( typeAroundSelectionAttribute ) { writer.addClass( positionToWidgetCssClass( typeAroundSelectionAttribute ), selectedViewElement ); } else { writer.removeClass( POSSIBLE_INSERTION_POSITIONS.map( positionToWidgetCssClass ), selectedViewElement ); } - } ); + }, { priority: 'highest' } ); function positionToWidgetCssClass( position ) { return `ck-widget_type-around_active_${ position }`; From 543d8a3529bea49e6046079ad02fcc9839ea4d04 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 21 May 2020 16:54:02 +0200 Subject: [PATCH 05/69] TMP --- packages/ckeditor5-widget/src/widget.js | 48 ----------------- .../src/widgettypearound/widgettypearound.js | 54 ++++++++++++++++--- 2 files changed, 48 insertions(+), 54 deletions(-) diff --git a/packages/ckeditor5-widget/src/widget.js b/packages/ckeditor5-widget/src/widget.js index 2c0517eb655..934a0bce001 100644 --- a/packages/ckeditor5-widget/src/widget.js +++ b/packages/ckeditor5-widget/src/widget.js @@ -12,7 +12,6 @@ import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobs import WidgetTypeAround from './widgettypearound/widgettypearound'; import { getLabel, isWidget, WIDGET_SELECTED_CLASS_NAME } from './utils'; import { - keyCodes, isArrowKeyCode, isForwardArrowKeyCode } from '@ckeditor/ckeditor5-utils/src/keyboard'; @@ -182,8 +181,6 @@ export default class Widget extends Plugin { const isForward = isForwardArrowKeyCode( keyCode, this.editor.locale.contentLanguageDirection ); wasHandled = this._handleArrowKeys( isForward ); - } else if ( keyCode === keyCodes.enter ) { - wasHandled = this._handleEnterKey( domEventData.shiftKey ); } if ( wasHandled ) { @@ -277,43 +274,6 @@ export default class Widget extends Plugin { } } - /** - * Handles the enter key, giving users and access to positions in the editable directly before - * (Shift+Enter) or after (Enter) the selected widget. - * It improves the UX, mainly when the widget is the first or last child of the root editable - * and there's no other way to type after or before it. - * - * @private - * @param {Boolean} isBackwards Set to true if the new paragraph is to be inserted before - * the selected widget (Shift+Enter). - * @returns {Boolean|undefined} Returns `true` if keys were handled correctly. - */ - _handleEnterKey( isBackwards ) { - const model = this.editor.model; - const modelSelection = model.document.selection; - const selectedElement = modelSelection.getSelectedElement(); - - if ( shouldInsertParagraph( selectedElement, model.schema ) ) { - model.change( writer => { - let position = writer.createPositionAt( selectedElement, isBackwards ? 'before' : 'after' ); - const paragraph = writer.createElement( 'paragraph' ); - - // Split the parent when inside a block element. - // https://github.com/ckeditor/ckeditor5/issues/1529 - if ( model.schema.isBlock( selectedElement.parent ) ) { - const paragraphLimit = model.schema.findAllowedParent( position, paragraph ); - - position = writer.split( position, paragraphLimit ).position; - } - - writer.insert( paragraph, position ); - writer.setSelection( paragraph, 'in' ); - } ); - - return true; - } - } - /** * Sets {@link module:engine/model/selection~Selection document's selection} over given element. * @@ -401,11 +361,3 @@ function isChild( element, parent ) { return Array.from( element.getAncestors() ).includes( parent ); } - -// Checks if enter key should insert paragraph. This should be done only on elements of type object (excluding inline objects). -// -// @param {module:engine/model/element~Element} element And element to check. -// @param {module:engine/model/schema~Schema} schema -function shouldInsertParagraph( element, schema ) { - return element && schema.isObject( element ) && !schema.isInline( element ); -} diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 1177556d33e..7c9b5f51fb2 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -97,6 +97,7 @@ export default class WidgetTypeAround extends Plugin { this._enableTypeAroundUIInjection(); this._enableDetectionOfTypeAroundWidgets(); this._enableInsertingParagraphsOnButtonClick(); + this._enableInsertingParagraphsOnEnterKeypress(); this._enableTypeAroundActivationUsingKeyboardArrows(); // TODO: This is a quick fix and it should be removed the proper integration arrives. @@ -334,37 +335,51 @@ export default class WidgetTypeAround extends Plugin { if ( shouldStopAndPreventDefault ) { domEventData.preventDefault(); - domEventData.stopPropagation(); evt.stop(); } } ); }, { priority: priorities.get( 'high' ) + 1 } ); modelSelection.on( 'change:range', () => { - const editor = this.editor; - const model = editor.model; - const modelSelection = model.document.selection; - if ( !modelSelection.hasAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ) { return; } + // Get rid of the widget type around attribute of the selection on every change:range. + // If the range changes, it means for sure, the user is no longer in the active ("blinking line") mode. editor.model.change( writer => { // TODO: use data.directChange to not break collaboration? writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); } ); + + // Also, if the range changes, get rid of CSS classes associated with the active ("blinking line") mode. + // There's no way to do that in the "selection" downcast dispatcher because it is executed too late. + editingView.change( writer => { + const selectedViewElement = editingView.document.selection.getSelectedElement(); + + writer.removeClass( POSSIBLE_INSERTION_POSITIONS.map( positionToWidgetCssClass ), selectedViewElement ); + } ); } ); + // React to changes of the mode selection attribute made by the arrow keys listener. + // If the block widget is selected and the attribute changes, downcast the attribute to special + // CSS classes associated with the active ("blinking line") mode of the widget. editor.editing.downcastDispatcher.on( 'selection', ( evt, data, conversionApi ) => { const writer = conversionApi.writer; const selectedModelElement = data.selection.getSelectedElement(); + + if ( !selectedModelElement ) { + return; + } + const selectedViewElement = conversionApi.mapper.toViewElement( selectedModelElement ); - const typeAroundSelectionAttribute = data.selection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); if ( !isTypeAroundWidget( selectedViewElement, selectedModelElement, schema ) ) { return; } + const typeAroundSelectionAttribute = data.selection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); + if ( typeAroundSelectionAttribute ) { writer.addClass( positionToWidgetCssClass( typeAroundSelectionAttribute ), selectedViewElement ); } else { @@ -376,6 +391,33 @@ export default class WidgetTypeAround extends Plugin { return `ck-widget_type-around_active_${ position }`; } } + + /** + * TODO + * + * @private + */ + _enableInsertingParagraphsOnEnterKeypress() { + const editor = this.editor; + const model = editor.model; + const modelSelection = model.document.selection; + const editingView = editor.editing.view; + + this.listenTo( editingView.document, 'enter', ( evt, domEventData ) => { + const typeAroundSelectionAttribute = modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); + + if ( !typeAroundSelectionAttribute ) { + return; + } + + const selectedViewElement = editingView.document.selection.getSelectedElement(); + + this._insertParagraph( selectedViewElement, typeAroundSelectionAttribute ); + + domEventData.preventDefault(); + evt.stop(); + } ); + } } // Injects the type around UI into a view widget instance. From e8c5e73c62f841e829dde5bf5ade745a17e3279b Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 27 May 2020 09:54:11 +0200 Subject: [PATCH 06/69] The model selectionPostFixer() should not alter the selection if a "fixed" range is the same as the original. --- .../ckeditor5-engine/src/model/utils/selection-post-fixer.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js b/packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js index e868bdf2a0f..54606f93ee5 100644 --- a/packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js +++ b/packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js @@ -82,7 +82,10 @@ function selectionPostFixer( writer, model ) { // Those ranges might overlap but will be corrected later. const correctedRange = tryFixingRange( modelRange, schema ); - if ( correctedRange ) { + // It may happen that a new range is returned but in fact it has the same positions as the original + // range anyway. If this branch was not discarded, a new selection would be set and that, for instance, + // would destroy the selection' attributes. + if ( correctedRange && !correctedRange.isEqual( modelRange ) ) { ranges.push( correctedRange ); wasFixed = true; } else { From b22aa4465b46c947905fca6663267cc6ef251cd6 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 27 May 2020 14:05:11 +0200 Subject: [PATCH 07/69] Used a thin caret-like look for type around lines. --- .../ckeditor5-widget/widgettypearound.css | 24 ++++++++++++------- .../theme/widgettypearound.css | 4 ++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css index da62d03e2c5..4a27c1cc98a 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css @@ -117,7 +117,7 @@ & .ck-widget__type-around__line { pointer-events: none; - height: var(--ck-widget-outline-thickness); + height: 1px; animation: ck-widget-type-around-line-pulse linear 1s infinite normal forwards; } @@ -128,6 +128,14 @@ outline-color: transparent; } } + + &.ck-widget_type-around_active_before, + &.ck-widget_type-around_active_after { + & > .ck-widget__type-around > .ck-widget__type-around__button { + pointer-events: none; + opacity: 0; + } + } } /* @@ -191,18 +199,18 @@ @keyframes ck-widget-type-around-line-pulse { 0% { - background: hsla(var(--ck-color-focus-border-coordinates), 0); + background: hsla(0, 0%, 0%, 0); } - 40% { - background: hsla(var(--ck-color-focus-border-coordinates), 0); + 49% { + background: hsla(0, 0%, 0%, 0); } 50% { - background: hsla(var(--ck-color-focus-border-coordinates), 1); + background: hsla(0, 0%, 0%, 1); } - 90% { - background: hsla(var(--ck-color-focus-border-coordinates), 1); + 99% { + background: hsla(0, 0%, 0%, 1); } 100% { - background: hsla(var(--ck-color-focus-border-coordinates), 0); + background: hsla(0, 0%, 0%, 0); } } diff --git a/packages/ckeditor5-widget/theme/widgettypearound.css b/packages/ckeditor5-widget/theme/widgettypearound.css index 07db3d99954..2fb18c9492f 100644 --- a/packages/ckeditor5-widget/theme/widgettypearound.css +++ b/packages/ckeditor5-widget/theme/widgettypearound.css @@ -72,11 +72,11 @@ right: 0; &.ck-widget__type-around__line_before { - top: calc(-1 * var(--ck-widget-outline-thickness)); + top: -1px; } &.ck-widget__type-around__line_after { - bottom: calc(-1 * var(--ck-widget-outline-thickness)); + bottom: -1px; } } From 824d7317de8461773fab4f55cdfd6c585498131d Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 27 May 2020 14:11:54 +0200 Subject: [PATCH 08/69] Show the buttons and lines for all block widgets regarless of their location. --- .../src/widgettypearound/utils.js | 38 --------- .../src/widgettypearound/widgettypearound.js | 77 +------------------ .../theme/widgettypearound.css | 11 --- 3 files changed, 2 insertions(+), 124 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/utils.js b/packages/ckeditor5-widget/src/widgettypearound/utils.js index a11956bfd72..adf777e448f 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/utils.js +++ b/packages/ckeditor5-widget/src/widgettypearound/utils.js @@ -55,41 +55,3 @@ export function getClosestWidgetViewElement( domElement, domConverter ) { return domConverter.mapDomToView( widgetDomElement ); } - -/** - * For the passed widget view element, this helper returns an array of positions which - * correspond to the "tight spots" around the widget which cannot be accessed due to - * limitations of selection rendering in web browsers. - * - * @param {module:engine/view/element~Element} widgetViewElement - * @returns {Array.} - */ -export function getWidgetTypeAroundPositions( widgetViewElement ) { - const positions = []; - - if ( isFirstChild( widgetViewElement ) || hasPreviousWidgetSibling( widgetViewElement ) ) { - positions.push( 'before' ); - } - - if ( isLastChild( widgetViewElement ) || hasNextWidgetSibling( widgetViewElement ) ) { - positions.push( 'after' ); - } - - return positions; -} - -function isFirstChild( widget ) { - return !widget.previousSibling; -} - -function isLastChild( widget ) { - return !widget.nextSibling; -} - -function hasPreviousWidgetSibling( widget ) { - return widget.previousSibling && isWidget( widget.previousSibling ); -} - -function hasNextWidgetSibling( widget ) { - return widget.nextSibling && isWidget( widget.nextSibling ); -} diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index a54e88afc12..41423873db9 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -20,7 +20,6 @@ import priorities from '@ckeditor/ckeditor5-utils/src/priorities'; import { isTypeAroundWidget, - getWidgetTypeAroundPositions, getClosestTypeAroundDomButton, getTypeAroundButtonPosition, getClosestWidgetViewElement @@ -64,38 +63,11 @@ export default class WidgetTypeAround extends Plugin { return 'WidgetTypeAround'; } - /** - * @inheritDoc - */ - constructor( editor ) { - super( editor ); - - /** - * A set containing all widgets in all editor roots that have the type around UI injected in - * {@link #_enableTypeAroundUIInjection}. - * - * Keeping track of them saves time, for instance, when updating their CSS classes. - * - * @private - * @readonly - * @member {Set} #_widgetsWithTypeAroundUI - */ - this._widgetsWithTypeAroundUI = new Set(); - } - - /** - * @inheritDoc - */ - destroy() { - this._widgetsWithTypeAroundUI.clear(); - } - /** * @inheritDoc */ init() { this._enableTypeAroundUIInjection(); - this._enableDetectionOfTypeAroundWidgets(); this._enableInsertingParagraphsOnButtonClick(); this._enableInsertingParagraphsOnEnterKeypress(); this._enableTypeAroundActivationUsingKeyboardArrows(); @@ -155,51 +127,10 @@ export default class WidgetTypeAround extends Plugin { // Filter out non-widgets and inline widgets. if ( isTypeAroundWidget( viewElement, data.item, schema ) ) { injectUIIntoWidget( conversionApi.writer, buttonTitles, viewElement ); - - // Keep track of widgets that have the type around UI injected. - // In the #_enableDetectionOfTypeAroundWidgets() we will iterate only over these - // widgets instead of all children of the root. This should improve the performance. - this._widgetsWithTypeAroundUI.add( viewElement ); } }, { priority: 'low' } ); } - /** - * Registers an editing view post-fixer which checks all block widgets in the content - * and adds CSS classes to these which should have the typing around (UI) enabled - * and visible for the users. - * - * @private - */ - _enableDetectionOfTypeAroundWidgets() { - const editor = this.editor; - const editingView = editor.editing.view; - - function positionToWidgetCssClass( position ) { - return `ck-widget_can-type-around_${ position }`; - } - - editingView.document.registerPostFixer( writer => { - for ( const widgetViewElement of this._widgetsWithTypeAroundUI ) { - // If the widget is no longer attached to the root (for instance, because it was removed), - // there is no need to update its classes and we can safely forget about it. - if ( !widgetViewElement.isAttached() ) { - this._widgetsWithTypeAroundUI.delete( widgetViewElement ); - } else { - // Update widgets' classes depending on possible positions for paragraph insertion. - const positions = getWidgetTypeAroundPositions( widgetViewElement ); - - // Remove all classes. In theory we could remove only these that will not be added a few lines later, - // but since there are only two... KISS. - writer.removeClass( POSSIBLE_INSERTION_POSITIONS.map( positionToWidgetCssClass ), widgetViewElement ); - - // Set CSS classes related to possible positions. They are used so the UI knows which buttons to display. - writer.addClass( positions.map( positionToWidgetCssClass ), widgetViewElement ); - } - } - } ); - } - /** * Registers a `mousedown` listener for the view document which intercepts events * coming from the type around UI, which happens when a user clicks one of the buttons @@ -289,14 +220,10 @@ export default class WidgetTypeAround extends Plugin { // If the selection didn't have the attribute, let's set it now according to the direction of the arrow // key press. This also means we cannot let the Widget plugin listener move the selection. else { - const widgetTypeAroundPositions = getWidgetTypeAroundPositions( selectedViewElement ); - - // Set the selection attribute only if the keystroke direction matches the type around position - // of the widget. - if ( isForward && widgetTypeAroundPositions.includes( 'after' ) ) { + if ( isForward ) { writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'after' ); shouldStopAndPreventDefault = true; - } else if ( !isForward && widgetTypeAroundPositions.includes( 'before' ) ) { + } else { writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' ); shouldStopAndPreventDefault = true; } diff --git a/packages/ckeditor5-widget/theme/widgettypearound.css b/packages/ckeditor5-widget/theme/widgettypearound.css index 2fb18c9492f..101405673dd 100644 --- a/packages/ckeditor5-widget/theme/widgettypearound.css +++ b/packages/ckeditor5-widget/theme/widgettypearound.css @@ -37,17 +37,6 @@ } } - /* - * Hide the type around buttons depending on which directions the widget supports. - */ - &:not(.ck-widget_can-type-around_before) > .ck-widget__type-around > .ck-widget__type-around__button_before { - display: none; - } - - &:not(.ck-widget_can-type-around_after) > .ck-widget__type-around > .ck-widget__type-around__button_after { - display: none; - } - /* * Styles for the buttons when: * - the widget is selected, From 767544b9044e0b6561626b08dbcc6ed618934fa9 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 27 May 2020 14:36:31 +0200 Subject: [PATCH 09/69] Enabled typing support when in the "fake caret" mode. --- .../utils/injectunsafekeystrokeshandling.js | 2 +- .../src/widgettypearound/widgettypearound.js | 175 ++++++++++-------- 2 files changed, 99 insertions(+), 78 deletions(-) diff --git a/packages/ckeditor5-typing/src/utils/injectunsafekeystrokeshandling.js b/packages/ckeditor5-typing/src/utils/injectunsafekeystrokeshandling.js index 7b5a33a9380..1ec4e31ab09 100644 --- a/packages/ckeditor5-typing/src/utils/injectunsafekeystrokeshandling.js +++ b/packages/ckeditor5-typing/src/utils/injectunsafekeystrokeshandling.js @@ -161,7 +161,7 @@ for ( let code = 112; code <= 135; code++ ) { // @private // @param {engine.view.observer.keyObserver.KeyEventData} keyData // @returns {Boolean} -function isSafeKeystroke( keyData ) { +export function isSafeKeystroke( keyData ) { // Keystrokes which contain Ctrl don't represent typing. if ( keyData.ctrlKey ) { return true; diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 41423873db9..e42ea4bd5e6 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -25,6 +25,10 @@ import { getClosestWidgetViewElement } from './utils'; +import { + isSafeKeystroke +} from '@ckeditor/ckeditor5-typing/src/utils/injectunsafekeystrokeshandling'; + import returnIcon from '../../theme/icons/return-arrow.svg'; import '../../theme/widgettypearound.css'; @@ -103,6 +107,21 @@ export default class WidgetTypeAround extends Plugin { editingView.scrollToTheSelection(); } + _insertParagraphAccordingToSelectionAttribute() { + const editor = this.editor; + const model = editor.model; + const editingView = editor.editing.view; + const typeAroundSelectionAttributeValue = model.document.selection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); + + if ( !typeAroundSelectionAttributeValue ) { + return; + } + + const selectedViewElement = editingView.document.selection.getSelectedElement(); + + this._insertParagraph( selectedViewElement, typeAroundSelectionAttributeValue ); + } + /** * Creates a listener in the editing conversion pipeline that injects the type around * UI into every single widget instance created in the editor. @@ -171,84 +190,30 @@ export default class WidgetTypeAround extends Plugin { // Note: The priority must precede the default Widget class keydown handler. editingView.document.on( 'keydown', ( evt, domEventData ) => { - const keyCode = domEventData.keyCode; - - if ( !isArrowKeyCode( keyCode ) ) { - return; + if ( !isSafeKeystroke( domEventData ) ) { + this._insertParagraphAccordingToSelectionAttribute(); + } else if ( isArrowKeyCode( domEventData.keyCode ) ) { + this._handleArrowKeyPress( evt, domEventData ); } - - const selectedViewElement = editingView.document.selection.getSelectedElement(); - const selectedModelElement = editor.editing.mapper.toModelElement( selectedViewElement ); - - if ( !isTypeAroundWidget( selectedViewElement, selectedModelElement, schema ) ) { - return; - } - - const isForward = isForwardArrowKeyCode( keyCode, editor.locale.contentLanguageDirection ); - const typeAroundSelectionAttribute = modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); - - editor.model.change( writer => { - let shouldStopAndPreventDefault; - - // If the selection already has the attribute... - if ( typeAroundSelectionAttribute ) { - const selectionPosition = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition(); - const nearestSelectionRange = schema.getNearestSelectionRange( selectionPosition, isForward ? 'forward' : 'backward' ); - const isLeavingWidget = typeAroundSelectionAttribute === ( isForward ? 'after' : 'before' ); - - // ...and the keyboard arrow matches the value of the selection attribute... - if ( isLeavingWidget ) { - // ...and if there is some place for the selection to go to... - if ( nearestSelectionRange ) { - // ...then just remove the attribute and let the default Widget plugin listener handle moving the selection. - writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); - } - - // If the selection had nowhere to go, let's leave the attribute as it was and pass through - // to the Widget plugin listener which will... in fact also do nothing. But this is no longer - // the problem of the WidgetTypeAround plugin. - } - // ...and the keyboard arrow works against the value of the selection attribute... - else { - // ...then remove the selection attribute but prevent default DOM actions - // and do not let the Widget plugin listener move the selection. This brings - // the widget back to the state, for instance, like if was selected using the mouse. - writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); - shouldStopAndPreventDefault = true; - } - } - // If the selection didn't have the attribute, let's set it now according to the direction of the arrow - // key press. This also means we cannot let the Widget plugin listener move the selection. - else { - if ( isForward ) { - writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'after' ); - shouldStopAndPreventDefault = true; - } else { - writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' ); - shouldStopAndPreventDefault = true; - } - } - - if ( shouldStopAndPreventDefault ) { - domEventData.preventDefault(); - evt.stop(); - } - } ); }, { priority: priorities.get( 'high' ) + 1 } ); + // This listener makes sure the widget type around selection attribute will be gone from the model + // selection as soon as the model range changes. This attribute only makes sense when a widget is selected + // (and the "fake horizontal caret" is visible) so whenever the range changes (e.g. selection moved somewhere else), + // let's get rid of the attribute so that the selection downcast dispatcher isn't even bothered. modelSelection.on( 'change:range', () => { if ( !modelSelection.hasAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ) { return; } // Get rid of the widget type around attribute of the selection on every change:range. - // If the range changes, it means for sure, the user is no longer in the active ("blinking line") mode. + // If the range changes, it means for sure, the user is no longer in the active ("fake horizontal caret") mode. editor.model.change( writer => { // TODO: use data.directChange to not break collaboration? writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); } ); - // Also, if the range changes, get rid of CSS classes associated with the active ("blinking line") mode. + // Also, if the range changes, get rid of CSS classes associated with the active ("fake horizontal caret") mode. // There's no way to do that in the "selection" downcast dispatcher because it is executed too late. editingView.change( writer => { const selectedViewElement = editingView.document.selection.getSelectedElement(); @@ -259,7 +224,7 @@ export default class WidgetTypeAround extends Plugin { // React to changes of the mode selection attribute made by the arrow keys listener. // If the block widget is selected and the attribute changes, downcast the attribute to special - // CSS classes associated with the active ("blinking line") mode of the widget. + // CSS classes associated with the active ("fake horizontal caret") mode of the widget. editor.editing.downcastDispatcher.on( 'selection', ( evt, data, conversionApi ) => { const writer = conversionApi.writer; const selectedModelElement = data.selection.getSelectedElement(); @@ -288,28 +253,84 @@ export default class WidgetTypeAround extends Plugin { } } - /** - * TODO - * - * @private - */ - _enableInsertingParagraphsOnEnterKeypress() { + _handleArrowKeyPress( evt, domEventData ) { const editor = this.editor; const model = editor.model; const modelSelection = model.document.selection; + const schema = model.schema; const editingView = editor.editing.view; - this.listenTo( editingView.document, 'enter', ( evt, domEventData ) => { - const typeAroundSelectionAttribute = modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); + const keyCode = domEventData.keyCode; + const selectedViewElement = editingView.document.selection.getSelectedElement(); + const selectedModelElement = editor.editing.mapper.toModelElement( selectedViewElement ); - if ( !typeAroundSelectionAttribute ) { - return; + if ( !isTypeAroundWidget( selectedViewElement, selectedModelElement, schema ) ) { + return; + } + + const isForward = isForwardArrowKeyCode( keyCode, editor.locale.contentLanguageDirection ); + const typeAroundSelectionAttribute = modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); + + editor.model.change( writer => { + let shouldStopAndPreventDefault; + + // If the selection already has the attribute... + if ( typeAroundSelectionAttribute ) { + const selectionPosition = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition(); + const nearestSelectionRange = schema.getNearestSelectionRange( selectionPosition, isForward ? 'forward' : 'backward' ); + const isLeavingWidget = typeAroundSelectionAttribute === ( isForward ? 'after' : 'before' ); + + // ...and the keyboard arrow matches the value of the selection attribute... + if ( isLeavingWidget ) { + // ...and if there is some place for the selection to go to... + if ( nearestSelectionRange ) { + // ...then just remove the attribute and let the default Widget plugin listener handle moving the selection. + writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); + } + + // If the selection had nowhere to go, let's leave the attribute as it was and pass through + // to the Widget plugin listener which will... in fact also do nothing. But this is no longer + // the problem of the WidgetTypeAround plugin. + } + // ...and the keyboard arrow works against the value of the selection attribute... + else { + // ...then remove the selection attribute but prevent default DOM actions + // and do not let the Widget plugin listener move the selection. This brings + // the widget back to the state, for instance, like if was selected using the mouse. + writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); + shouldStopAndPreventDefault = true; + } + } + // If the selection didn't have the attribute, let's set it now according to the direction of the arrow + // key press. This also means we cannot let the Widget plugin listener move the selection. + else { + if ( isForward ) { + writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'after' ); + shouldStopAndPreventDefault = true; + } else { + writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' ); + shouldStopAndPreventDefault = true; + } } - const selectedViewElement = editingView.document.selection.getSelectedElement(); + if ( shouldStopAndPreventDefault ) { + domEventData.preventDefault(); + evt.stop(); + } + } ); + } - this._insertParagraph( selectedViewElement, typeAroundSelectionAttribute ); + /** + * TODO + * + * @private + */ + _enableInsertingParagraphsOnEnterKeypress() { + const editor = this.editor; + const editingView = editor.editing.view; + this.listenTo( editingView.document, 'enter', ( evt, domEventData ) => { + this._insertParagraphAccordingToSelectionAttribute(); domEventData.preventDefault(); evt.stop(); } ); From 5dc4962f7085b23cc099be0c8f5ae175ccd25c27 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 27 May 2020 14:51:20 +0200 Subject: [PATCH 10/69] Code refactoring. --- .../src/widgettypearound/widgettypearound.js | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index e42ea4bd5e6..8a0a18def2b 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -74,6 +74,7 @@ export default class WidgetTypeAround extends Plugin { this._enableTypeAroundUIInjection(); this._enableInsertingParagraphsOnButtonClick(); this._enableInsertingParagraphsOnEnterKeypress(); + this._enableInsertingParagraphsOnUnsafeKeystroke(); this._enableTypeAroundActivationUsingKeyboardArrows(); } @@ -107,6 +108,11 @@ export default class WidgetTypeAround extends Plugin { editingView.scrollToTheSelection(); } + /** + * TODO + * + * @private + */ _insertParagraphAccordingToSelectionAttribute() { const editor = this.editor; const model = editor.model; @@ -190,9 +196,7 @@ export default class WidgetTypeAround extends Plugin { // Note: The priority must precede the default Widget class keydown handler. editingView.document.on( 'keydown', ( evt, domEventData ) => { - if ( !isSafeKeystroke( domEventData ) ) { - this._insertParagraphAccordingToSelectionAttribute(); - } else if ( isArrowKeyCode( domEventData.keyCode ) ) { + if ( isArrowKeyCode( domEventData.keyCode ) ) { this._handleArrowKeyPress( evt, domEventData ); } }, { priority: priorities.get( 'high' ) + 1 } ); @@ -253,6 +257,11 @@ export default class WidgetTypeAround extends Plugin { } } + /** + * TODO + * + * @private + */ _handleArrowKeyPress( evt, domEventData ) { const editor = this.editor; const model = editor.model; @@ -335,6 +344,24 @@ export default class WidgetTypeAround extends Plugin { evt.stop(); } ); } + + /** + * TODO + * + * @private + */ + _enableInsertingParagraphsOnUnsafeKeystroke() { + const editor = this.editor; + const editingView = editor.editing.view; + + // Note: The priority must precede the default Widget class keydown handler. + editingView.document.on( 'keydown', ( evt, domEventData ) => { + if ( !isSafeKeystroke( domEventData ) ) { + // TODO: Extra undo step problem. + this._insertParagraphAccordingToSelectionAttribute(); + } + }, { priority: priorities.get( 'high' ) + 1 } ); + } } // Injects the type around UI into a view widget instance. From 6f179b009be1a0aff8e2be5c347156163b277111 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 27 May 2020 16:12:59 +0200 Subject: [PATCH 11/69] Prevent default action on enter only if inserted a new paragraph. --- .../src/widgettypearound/widgettypearound.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 8a0a18def2b..7b274ca49ea 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -120,12 +120,14 @@ export default class WidgetTypeAround extends Plugin { const typeAroundSelectionAttributeValue = model.document.selection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); if ( !typeAroundSelectionAttributeValue ) { - return; + return null; } const selectedViewElement = editingView.document.selection.getSelectedElement(); this._insertParagraph( selectedViewElement, typeAroundSelectionAttributeValue ); + + return true; } /** @@ -339,9 +341,10 @@ export default class WidgetTypeAround extends Plugin { const editingView = editor.editing.view; this.listenTo( editingView.document, 'enter', ( evt, domEventData ) => { - this._insertParagraphAccordingToSelectionAttribute(); - domEventData.preventDefault(); - evt.stop(); + if ( this._insertParagraphAccordingToSelectionAttribute() ) { + domEventData.preventDefault(); + evt.stop(); + } } ); } From d5733aa014f970607cf2a080c83314874849fcda Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 27 May 2020 16:15:56 +0200 Subject: [PATCH 12/69] Animate the fake horizontal care from the full visibility onwards for faster feedback and better UX when navigating the document. --- .../theme/ckeditor5-widget/widgettypearound.css | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css index 4a27c1cc98a..9080da6a1ce 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css @@ -199,18 +199,18 @@ @keyframes ck-widget-type-around-line-pulse { 0% { - background: hsla(0, 0%, 0%, 0); + background: hsla(0, 0%, 0%, 1); } 49% { - background: hsla(0, 0%, 0%, 0); + background: hsla(0, 0%, 0%, 1); } 50% { - background: hsla(0, 0%, 0%, 1); + background: hsla(0, 0%, 0%, 0); } 99% { - background: hsla(0, 0%, 0%, 1); + background: hsla(0, 0%, 0%, 0); } 100% { - background: hsla(0, 0%, 0%, 0); + background: hsla(0, 0%, 0%, 1); } } From dfbf6948e29abaa6321638be43a757d605fb2c1e Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 27 May 2020 16:18:40 +0200 Subject: [PATCH 13/69] Remove the fake horizontal caret as soon as the editor is blurred. The regular caret disappears and so should the fake one. --- .../src/widgettypearound/widgettypearound.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 7b274ca49ea..ab0e68fb7fa 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -254,6 +254,15 @@ export default class WidgetTypeAround extends Plugin { } }, { priority: 'highest' } ); + this.listenTo( editor.ui.focusTracker, 'change:isFocused', ( evt, name, isFocused ) => { + if ( !isFocused ) { + editor.model.change( writer => { + // TODO: use data.directChange to not break collaboration? + writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); + } ); + } + } ); + function positionToWidgetCssClass( position ) { return `ck-widget_type-around_active_${ position }`; } From a81a9cb6bbd17857fcf9b1b16e9dfdaaeede3114 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 27 May 2020 16:25:05 +0200 Subject: [PATCH 14/69] The keydown and enter listeners should not collide. --- .../src/widgettypearound/widgettypearound.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index ab0e68fb7fa..03b593f5d1e 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -14,7 +14,8 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Template from '@ckeditor/ckeditor5-ui/src/template'; import { isArrowKeyCode, - isForwardArrowKeyCode + isForwardArrowKeyCode, + keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; import priorities from '@ckeditor/ckeditor5-utils/src/priorities'; @@ -120,7 +121,7 @@ export default class WidgetTypeAround extends Plugin { const typeAroundSelectionAttributeValue = model.document.selection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); if ( !typeAroundSelectionAttributeValue ) { - return null; + return false; } const selectedViewElement = editingView.document.selection.getSelectedElement(); @@ -368,7 +369,8 @@ export default class WidgetTypeAround extends Plugin { // Note: The priority must precede the default Widget class keydown handler. editingView.document.on( 'keydown', ( evt, domEventData ) => { - if ( !isSafeKeystroke( domEventData ) ) { + // Don't handle enter here. It's handled in a separate listener. + if ( domEventData.keyCode !== keyCodes.enter && !isSafeKeystroke( domEventData ) ) { // TODO: Extra undo step problem. this._insertParagraphAccordingToSelectionAttribute(); } From a641a037cba05d660061c80b28a7645952de3f0c Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 28 May 2020 15:01:20 +0200 Subject: [PATCH 15/69] Added an outline to the type around fake caret for better visibility on dark backgrounds. --- .../theme/ckeditor5-widget/widgettypearound.css | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css index 9080da6a1ce..d3e00dce177 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css @@ -119,6 +119,8 @@ pointer-events: none; height: 1px; animation: ck-widget-type-around-line-pulse linear 1s infinite normal forwards; + outline: solid 1px hsla(0, 0%, 100%, .5); + background: var(--ck-color-base-text); } &.ck-widget_selected, @@ -199,18 +201,18 @@ @keyframes ck-widget-type-around-line-pulse { 0% { - background: hsla(0, 0%, 0%, 1); + opacity: 1; } 49% { - background: hsla(0, 0%, 0%, 1); + opacity: 1; } 50% { - background: hsla(0, 0%, 0%, 0); + opacity: 0; } 99% { - background: hsla(0, 0%, 0%, 0); + opacity: 0; } 100% { - background: hsla(0, 0%, 0%, 1); + opacity: 1; } } From 96609c4b3ec9e11085fe6011781b4b64712b800f Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 28 May 2020 15:14:44 +0200 Subject: [PATCH 16/69] Tests: Added table and table cell properties to the widget type around manual test. --- packages/ckeditor5-widget/tests/manual/type-around.html | 4 ++-- packages/ckeditor5-widget/tests/manual/type-around.js | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-widget/tests/manual/type-around.html b/packages/ckeditor5-widget/tests/manual/type-around.html index 908f9bd8fbb..2d95d48518b 100644 --- a/packages/ckeditor5-widget/tests/manual/type-around.html +++ b/packages/ckeditor5-widget/tests/manual/type-around.html @@ -39,13 +39,13 @@

Heading 1

 
-
+
  - +

 

bar
Caption
diff --git a/packages/ckeditor5-widget/tests/manual/type-around.js b/packages/ckeditor5-widget/tests/manual/type-around.js index 5513de7a47e..a51ae2a039e 100644 --- a/packages/ckeditor5-widget/tests/manual/type-around.js +++ b/packages/ckeditor5-widget/tests/manual/type-around.js @@ -9,6 +9,8 @@ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor' import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline'; import MediaEmbed from '@ckeditor/ckeditor5-media-embed/src/mediaembed'; +import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties'; +import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; @@ -96,7 +98,7 @@ document.querySelector( '#toggleReadOnly' ).addEventListener( 'click', () => { ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ ArticlePluginSet, HorizontalLine, InlineWidget, MediaEmbed ], + plugins: [ ArticlePluginSet, HorizontalLine, InlineWidget, MediaEmbed, TableProperties, TableCellProperties ], toolbar: [ 'heading', '|', @@ -123,7 +125,9 @@ ClassicEditor contentToolbar: [ 'tableColumn', 'tableRow', - 'mergeTableCells' + 'mergeTableCells', + 'tableProperties', + 'tableCellProperties' ] } } ) From 34a2b618c099bc0bbb24ca4963d2530afd940607 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 28 May 2020 16:04:48 +0200 Subject: [PATCH 17/69] Integrated the fake horizontal caret with the widget selection handle. --- .../theme/ckeditor5-widget/widgettypearound.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css index d3e00dce177..96d93d2f179 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css @@ -138,6 +138,14 @@ opacity: 0; } } + + /* + * Fake horizontal caret integration with the selection handle. When the caret is visible, simply + * hide the handle because it makes no sense and intersects with the caret. + */ + &.ck-widget_type-around_active_before.ck-widget_with-selection-handle > .ck-widget__selection-handle { + display: none; + } } /* From d3b310704ae17e7c23828c23f018a8e4dbcf1500 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 2 Jun 2020 16:13:48 +0200 Subject: [PATCH 18/69] Made some of Widget plugin methods @protected instead of @private because they are used in WidgetTypeAround now. --- packages/ckeditor5-widget/src/widget.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-widget/src/widget.js b/packages/ckeditor5-widget/src/widget.js index 934a0bce001..82ef55f04b6 100644 --- a/packages/ckeditor5-widget/src/widget.js +++ b/packages/ckeditor5-widget/src/widget.js @@ -277,7 +277,7 @@ export default class Widget extends Plugin { /** * Sets {@link module:engine/model/selection~Selection document's selection} over given element. * - * @private + * @protected * @param {module:engine/model/element~Element} element */ _setSelectionOverElement( element ) { @@ -291,7 +291,7 @@ export default class Widget extends Plugin { * {@link module:engine/model/selection~Selection model selection} exists and is marked in * {@link module:engine/model/schema~Schema schema} as `object`. * - * @private + * @protected * @param {Boolean} forward Direction of checking. * @returns {module:engine/model/element~Element|null} */ From bc3a51a831e85791e13ba4e2d49091fad60da07c Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 2 Jun 2020 16:14:17 +0200 Subject: [PATCH 19/69] Added support for keyboard arrows handling when selection the widget "from the outside". --- .../src/widgettypearound/widgettypearound.js | 117 +++++++++++------- 1 file changed, 70 insertions(+), 47 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 03b593f5d1e..6fc10a4ba52 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -282,63 +282,86 @@ export default class WidgetTypeAround extends Plugin { const editingView = editor.editing.view; const keyCode = domEventData.keyCode; + const isForward = isForwardArrowKeyCode( keyCode, editor.locale.contentLanguageDirection ); const selectedViewElement = editingView.document.selection.getSelectedElement(); const selectedModelElement = editor.editing.mapper.toModelElement( selectedViewElement ); - - if ( !isTypeAroundWidget( selectedViewElement, selectedModelElement, schema ) ) { - return; - } - - const isForward = isForwardArrowKeyCode( keyCode, editor.locale.contentLanguageDirection ); - const typeAroundSelectionAttribute = modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); - - editor.model.change( writer => { - let shouldStopAndPreventDefault; - - // If the selection already has the attribute... - if ( typeAroundSelectionAttribute ) { - const selectionPosition = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition(); - const nearestSelectionRange = schema.getNearestSelectionRange( selectionPosition, isForward ? 'forward' : 'backward' ); - const isLeavingWidget = typeAroundSelectionAttribute === ( isForward ? 'after' : 'before' ); - - // ...and the keyboard arrow matches the value of the selection attribute... - if ( isLeavingWidget ) { - // ...and if there is some place for the selection to go to... - if ( nearestSelectionRange ) { - // ...then just remove the attribute and let the default Widget plugin listener handle moving the selection. + let shouldStopAndPreventDefault; + + // Handle keyboard navigation when a type-around-compatible widget is currently selected. + if ( isTypeAroundWidget( selectedViewElement, selectedModelElement, schema ) ) { + const typeAroundSelectionAttribute = modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); + + model.change( writer => { + // If the selection already has the attribute... + if ( typeAroundSelectionAttribute ) { + const selectionPosition = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition(); + const nearestSelectionRange = schema.getNearestSelectionRange( selectionPosition, isForward ? 'forward' : 'backward' ); + const isLeavingWidget = typeAroundSelectionAttribute === ( isForward ? 'after' : 'before' ); + + // ...and the keyboard arrow matches the value of the selection attribute... + if ( isLeavingWidget ) { + // ...and if there is some place for the selection to go to... + if ( nearestSelectionRange ) { + // ...then just remove the attribute and let the default Widget plugin listener handle moving the selection. + writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); + } + + // If the selection had nowhere to go, let's leave the attribute as it was and pass through + // to the Widget plugin listener which will... in fact also do nothing. But this is no longer + // the problem of the WidgetTypeAround plugin. + } + // ...and the keyboard arrow works against the value of the selection attribute... + else { + // ...then remove the selection attribute but prevent default DOM actions + // and do not let the Widget plugin listener move the selection. This brings + // the widget back to the state, for instance, like if was selected using the mouse. writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); + shouldStopAndPreventDefault = true; } - - // If the selection had nowhere to go, let's leave the attribute as it was and pass through - // to the Widget plugin listener which will... in fact also do nothing. But this is no longer - // the problem of the WidgetTypeAround plugin. } - // ...and the keyboard arrow works against the value of the selection attribute... + // If the selection didn't have the attribute, let's set it now according to the direction of the arrow + // key press. This also means we cannot let the Widget plugin listener move the selection. else { - // ...then remove the selection attribute but prevent default DOM actions - // and do not let the Widget plugin listener move the selection. This brings - // the widget back to the state, for instance, like if was selected using the mouse. - writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); - shouldStopAndPreventDefault = true; - } - } - // If the selection didn't have the attribute, let's set it now according to the direction of the arrow - // key press. This also means we cannot let the Widget plugin listener move the selection. - else { - if ( isForward ) { - writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'after' ); - shouldStopAndPreventDefault = true; - } else { - writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' ); + if ( isForward ) { + writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'after' ); + } else { + writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' ); + } + shouldStopAndPreventDefault = true; } - } + } ); + } + // Handle keyboard arrow navigation when the selection is next to a type-around-compatible widget + // and the widget is about to be selected. + // + // This code mirrors the implementation from the Widget plugin but also adds the selection attribute. + // Unfortunately, there's no safe way to let the Widget plugin do the selection part first + // and then just set the selection attribute here in the WidgetTypeAround plugin. This is why + // this code must duplicate some from the Widget plugin. + else if ( modelSelection.isCollapsed ) { + const widgetPlugin = editor.plugins.get( 'Widget' ); + + // This is the widget the selection is about to be set on. + const modelElementNextToSelection = widgetPlugin._getObjectElementNextToSelection( isForward ); + const viewElementNextToSelection = editor.editing.mapper.toViewElement( modelElementNextToSelection ); + + if ( isTypeAroundWidget( viewElementNextToSelection, modelElementNextToSelection, schema ) ) { + model.change( writer => { + widgetPlugin._setSelectionOverElement( modelElementNextToSelection ); + writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, isForward ? 'before' : 'after' ); + } ); - if ( shouldStopAndPreventDefault ) { - domEventData.preventDefault(); - evt.stop(); + // The change() block above does the same job as the Widget plugin. The event can + // be safely canceled. + shouldStopAndPreventDefault = true; } - } ); + } + + if ( shouldStopAndPreventDefault ) { + domEventData.preventDefault(); + evt.stop(); + } } /** From e9d5a0d3516c3513d46798fda9e243f7bc9be080 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 2 Jun 2020 16:41:44 +0200 Subject: [PATCH 20/69] Tests: Added the selection-post-fixer test to make sure it does not set the new selection if no range was "post-fixed". --- .../src/model/utils/selection-post-fixer.js | 10 +++++-- .../tests/model/utils/selection-post-fixer.js | 29 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js b/packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js index 54606f93ee5..9c60f7b0471 100644 --- a/packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js +++ b/packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js @@ -82,9 +82,13 @@ function selectionPostFixer( writer, model ) { // Those ranges might overlap but will be corrected later. const correctedRange = tryFixingRange( modelRange, schema ); - // It may happen that a new range is returned but in fact it has the same positions as the original - // range anyway. If this branch was not discarded, a new selection would be set and that, for instance, - // would destroy the selection' attributes. + // "Selection fixing" algorithms sometimes get lost. In consequence, it may happen + // that a new range is returned but, in fact, it has the same positions as the original + // range anyway. If this range is not discarded, a new selection will be set and that, + // for instance, would destroy the selection attributes. Let's make sure that the post-fixer + // actually worked first before setting a new selection. + // + // https://github.com/ckeditor/ckeditor5/issues/6693 if ( correctedRange && !correctedRange.isEqual( modelRange ) ) { ranges.push( correctedRange ); wasFixed = true; diff --git a/packages/ckeditor5-engine/tests/model/utils/selection-post-fixer.js b/packages/ckeditor5-engine/tests/model/utils/selection-post-fixer.js index 0b43ae4b81c..a7d87b40215 100644 --- a/packages/ckeditor5-engine/tests/model/utils/selection-post-fixer.js +++ b/packages/ckeditor5-engine/tests/model/utils/selection-post-fixer.js @@ -835,6 +835,35 @@ describe( 'Selection post-fixer', () => { ']' ); } ); + + it( 'should not reset the selection if the final range is the same as the initial one', () => { + setModelData( model, + '' + + '' + + '[]' + + '' + + '
' + ); + + // Setting a selection attribute will trigger the post-fixer. However, because this + // action does not affect ranges, the post-fixer should not set a new selection and, + // in consequence, should not clear the selection attribute (like it normally would when + // a new selection is set). + // https://github.com/ckeditor/ckeditor5/issues/6693 + model.change( writer => { + writer.setSelectionAttribute( 'foo', 'bar' ); + } ); + + assertEqualMarkup( getModelData( model ), + '' + + '' + + '[]' + + '' + + '
' + ); + + expect( model.document.selection.hasAttribute( 'foo' ) ).to.be.true; + } ); } ); describe( 'non-collapsed selection - image scenarios', () => { From 7e98beef84018073cc9e9819e6d6e5279a0f11fe Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 2 Jun 2020 16:58:05 +0200 Subject: [PATCH 21/69] Tests: Added tests for new keyboard helpers. --- packages/ckeditor5-utils/src/keyboard.js | 16 +-- packages/ckeditor5-utils/tests/keyboard.js | 109 ++++++++++++++++++++- 2 files changed, 116 insertions(+), 9 deletions(-) diff --git a/packages/ckeditor5-utils/src/keyboard.js b/packages/ckeditor5-utils/src/keyboard.js index 542de2ad631..8bcbb82aa40 100644 --- a/packages/ckeditor5-utils/src/keyboard.js +++ b/packages/ckeditor5-utils/src/keyboard.js @@ -143,16 +143,16 @@ export function isArrowKeyCode( keyCode ) { } /** - * Returns the direction from the provided key code considering the language direction of the - * editor content. + * Returns the direction in which the {@link module:engine/model/documentselection~DocumentSelection selection} + * will move when a provided arrow key code is pressed considering the language direction of the editor content. * - * For instance, in right–to–left (RTL) languages, pressing the left arrow means moving selection right (forward) + * For instance, in right–to–left (RTL) content languages, pressing the left arrow means moving selection right (forward) * in the model structure. Similarly, pressing the right arrow moves the selection left (backward). * * @param {Number} keyCode - * @param {String} contentLanguageDirection The content language direction, corresponding to + * @param {'ltr'|'rtl'} contentLanguageDirection The content language direction, corresponding to * {@link module:utils/locale~Locale#contentLanguageDirection}. - * @returns {'left'|'up'|'right'|'down'} Arrow direction. + * @returns {'left'|'up'|'right'|'down'} Localized arrow direction. */ export function getLocalizedArrowKeyCodeDirection( keyCode, contentLanguageDirection ) { const isLtrContent = contentLanguageDirection === 'ltr'; @@ -173,14 +173,14 @@ export function getLocalizedArrowKeyCodeDirection( keyCode, contentLanguageDirec } /** - * Determines if the provided key code moves the selection forward or backward considering - * the language direction of the editor content. + * Determines if the provided key code moves the {@link module:engine/model/documentselection~DocumentSelection selection} + * forward or backward considering the language direction of the editor content. * * For instance, in right–to–left (RTL) languages, pressing the left arrow means moving forward * in the model structure. Similarly, pressing the right arrow moves the selection backward. * * @param {Number} keyCode - * @param {String} contentLanguageDirection The content language direction, corresponding to + * @param {'ltr'|'rtl'} contentLanguageDirection The content language direction, corresponding to * {@link module:utils/locale~Locale#contentLanguageDirection}. * @returns {Boolean} */ diff --git a/packages/ckeditor5-utils/tests/keyboard.js b/packages/ckeditor5-utils/tests/keyboard.js index 6136fb30cdd..d0fda51eda9 100644 --- a/packages/ckeditor5-utils/tests/keyboard.js +++ b/packages/ckeditor5-utils/tests/keyboard.js @@ -4,7 +4,15 @@ */ import env from '../src/env'; -import { keyCodes, getCode, parseKeystroke, getEnvKeystrokeText } from '../src/keyboard'; +import { + keyCodes, + getCode, + parseKeystroke, + getEnvKeystrokeText, + isArrowKeyCode, + isForwardArrowKeyCode, + getLocalizedArrowKeyCodeDirection +} from '../src/keyboard'; import { expectToThrowCKEditorError } from './_utils/utils'; describe( 'Keyboard', () => { @@ -160,4 +168,103 @@ describe( 'Keyboard', () => { } ); } ); } ); + + describe( 'isArrowKeyCode()', () => { + it( 'should return "true" for right arrow', () => { + expect( isArrowKeyCode( keyCodes.arrowright ) ).to.be.true; + } ); + + it( 'should return "true" for left arrow', () => { + expect( isArrowKeyCode( keyCodes.arrowleft ) ).to.be.true; + } ); + + it( 'should return "true" for up arrow', () => { + expect( isArrowKeyCode( keyCodes.arrowup ) ).to.be.true; + } ); + + it( 'should return "true" for down arrow', () => { + expect( isArrowKeyCode( keyCodes.arrowdown ) ).to.be.true; + } ); + + it( 'should return "false" for non-arrow keystrokes', () => { + expect( isArrowKeyCode( keyCodes.a ) ).to.be.false; + expect( isArrowKeyCode( keyCodes.ctrl ) ).to.be.false; + } ); + } ); + + describe( 'getLocalizedArrowKeyCodeDirection()', () => { + describe( 'for a left–to–right content language direction', () => { + it( 'should return "left" for left arrow', () => { + expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowleft, 'ltr' ) ).to.equal( 'left' ); + } ); + + it( 'should return "right" for right arrow', () => { + expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowright, 'ltr' ) ).to.equal( 'right' ); + } ); + + it( 'should return "up" for up arrow', () => { + expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowup, 'ltr' ) ).to.equal( 'up' ); + } ); + + it( 'should return "down" for down arrow', () => { + expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowdown, 'ltr' ) ).to.equal( 'down' ); + } ); + } ); + + describe( 'for a right-to-left content language direction', () => { + it( 'should return "right" for left arrow', () => { + expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowleft, 'rtl' ) ).to.equal( 'right' ); + } ); + + it( 'should return "left" for right arrow', () => { + expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowright, 'rtl' ) ).to.equal( 'left' ); + } ); + + it( 'should return "up" for up arrow', () => { + expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowup, 'rtl' ) ).to.equal( 'up' ); + } ); + + it( 'should return "down" for down arrow', () => { + expect( getLocalizedArrowKeyCodeDirection( keyCodes.arrowdown, 'rtl' ) ).to.equal( 'down' ); + } ); + } ); + } ); + + describe( 'isForwardArrowKeyCode()', () => { + describe( 'for a left–to–right content language direction', () => { + it( 'should return "true" for down arrow', () => { + expect( isForwardArrowKeyCode( keyCodes.arrowdown, 'ltr' ) ).to.be.true; + } ); + + it( 'should return "true" for right arrow', () => { + expect( isForwardArrowKeyCode( keyCodes.arrowright, 'ltr' ) ).to.be.true; + } ); + + it( 'should return "false" for up arrow', () => { + expect( isForwardArrowKeyCode( keyCodes.arrowup, 'ltr' ) ).to.be.false; + } ); + + it( 'should return "false" for left arrow', () => { + expect( isForwardArrowKeyCode( keyCodes.arrowleft, 'ltr' ) ).to.be.false; + } ); + } ); + + describe( 'for a right-to-left content language direction', () => { + it( 'should return "true" for down arrow', () => { + expect( isForwardArrowKeyCode( keyCodes.arrowdown, 'rtl' ) ).to.be.true; + } ); + + it( 'should return "true" for left arrow', () => { + expect( isForwardArrowKeyCode( keyCodes.arrowleft, 'rtl' ) ).to.be.true; + } ); + + it( 'should return "false" for up arrow', () => { + expect( isForwardArrowKeyCode( keyCodes.arrowup, 'rtl' ) ).to.be.false; + } ); + + it( 'should return "false" for right arrow', () => { + expect( isForwardArrowKeyCode( keyCodes.arrowright, 'rtl' ) ).to.be.false; + } ); + } ); + } ); } ); From da795f971ef03be5777f6c05553187387b5eb9e2 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 3 Jun 2020 10:05:48 +0200 Subject: [PATCH 22/69] Code refactoring in styles. Use a single fake caret per-widget. --- .../ckeditor5-widget/widgettypearound.css | 58 +++++++++++++------ .../src/widgettypearound/widgettypearound.js | 29 +++++----- .../theme/widgettypearound.css | 25 ++++---- 3 files changed, 67 insertions(+), 45 deletions(-) diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css index 96d93d2f179..e72c456be09 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css @@ -13,6 +13,16 @@ --ck-color-widget-type-around-button-icon: var(--ck-color-base-background); } +@define-mixin ck-widget-type-around-button-visible { + opacity: 1; + pointer-events: auto; +} + +@define-mixin ck-widget-type-around-button-hidden { + opacity: 0; + pointer-events: none; +} + .ck .ck-widget { /* * Styles of the type around buttons @@ -22,11 +32,10 @@ height: var(--ck-widget-type-around-button-size); background: var(--ck-color-widget-type-around-button); border-radius: 100px; - - pointer-events: none; - opacity: 0; transition: opacity var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve), background var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve); + @mixin ck-widget-type-around-button-hidden; + & svg { width: 10px; height: 8px; @@ -77,8 +86,7 @@ &.ck-widget_selected, &:hover { & > .ck-widget__type-around > .ck-widget__type-around__button { - pointer-events: auto; - opacity: 1; + @mixin ck-widget-type-around-button-visible; } } @@ -115,35 +123,50 @@ margin-left: 20px; } - & .ck-widget__type-around__line { + /* + * Styles for the horizontal "fake caret" which is displayed when the user navigates using the keyboard. + */ + & .ck-widget__type-around__fake-caret { pointer-events: none; height: 1px; - animation: ck-widget-type-around-line-pulse linear 1s infinite normal forwards; + animation: ck-widget-type-around-fake-caret-pulse linear 1s infinite normal forwards; + + /* + * The semit-transparent-outline+background combo improves the contrast + * when the background underneath the fake caret is dark. + */ outline: solid 1px hsla(0, 0%, 100%, .5); background: var(--ck-color-base-text); } + /* + * Styles of the widget when the "fake caret" is blinking (e.g. upon keyboard navigation). + * Despite the widget being physically selected in the model, its outline should disappear. + */ &.ck-widget_selected, &.ck-widget.ck-widget_selected:hover { - &.ck-widget_type-around_active_before, - &.ck-widget_type-around_active_after { + &.ck-widget_type-around_show-fake-caret_before, + &.ck-widget_type-around_show-fake-caret_after { outline-color: transparent; } } - &.ck-widget_type-around_active_before, - &.ck-widget_type-around_active_after { + /* + * Styles of the type around buttons when the "fake caret" is blinking (e.g. upon keyboard navigation). + * In this state, the type around buttons would collide with the fake carets so they should disappear. + */ + &.ck-widget_type-around_show-fake-caret_before, + &.ck-widget_type-around_show-fake-caret_after { & > .ck-widget__type-around > .ck-widget__type-around__button { - pointer-events: none; - opacity: 0; + @mixin ck-widget-type-around-button-hidden; } } /* * Fake horizontal caret integration with the selection handle. When the caret is visible, simply - * hide the handle because it makes no sense and intersects with the caret. + * hide the handle because it intersects with the caret (and does not make much sense anyway). */ - &.ck-widget_type-around_active_before.ck-widget_with-selection-handle > .ck-widget__selection-handle { + &.ck-widget_type-around_show-fake-caret_before.ck-widget_with-selection-handle > .ck-widget__selection-handle { display: none; } } @@ -159,8 +182,7 @@ &.ck-widget_selected, &:hover { & > .ck-widget__type-around > .ck-widget__type-around__button { - pointer-events: none; - opacity: 0; + @mixin ck-widget-type-around-button-hidden; } } } @@ -207,7 +229,7 @@ } } -@keyframes ck-widget-type-around-line-pulse { +@keyframes ck-widget-type-around-fake-caret-pulse { 0% { opacity: 1; } diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 6fc10a4ba52..371b8d4f623 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -265,7 +265,7 @@ export default class WidgetTypeAround extends Plugin { } ); function positionToWidgetCssClass( position ) { - return `ck-widget_type-around_active_${ position }`; + return `ck-widget_type-around_show-fake-caret_${ position }`; } } @@ -413,7 +413,7 @@ function injectUIIntoWidget( viewWriter, buttonTitles, widgetViewElement ) { const wrapperDomElement = this.toDomElement( domDocument ); injectButtons( wrapperDomElement, buttonTitles ); - injectLines( wrapperDomElement ); + injectFakeCaret( wrapperDomElement ); return wrapperDomElement; } ); @@ -450,19 +450,16 @@ function injectButtons( wrapperDomElement, buttonTitles ) { } // @param {HTMLElement} wrapperDomElement -function injectLines( wrapperDomElement ) { - for ( const position of POSSIBLE_INSERTION_POSITIONS ) { - const lineTemplate = new Template( { - tag: 'div', - attributes: { - class: [ - 'ck', - 'ck-widget__type-around__line', - `ck-widget__type-around__line_${ position }` - ] - } - } ); +function injectFakeCaret( wrapperDomElement ) { + const caretTemplate = new Template( { + tag: 'div', + attributes: { + class: [ + 'ck', + 'ck-widget__type-around__fake-caret' + ] + } + } ); - wrapperDomElement.appendChild( lineTemplate.render() ); - } + wrapperDomElement.appendChild( caretTemplate.render() ); } diff --git a/packages/ckeditor5-widget/theme/widgettypearound.css b/packages/ckeditor5-widget/theme/widgettypearound.css index 101405673dd..67639f3b1ee 100644 --- a/packages/ckeditor5-widget/theme/widgettypearound.css +++ b/packages/ckeditor5-widget/theme/widgettypearound.css @@ -54,26 +54,29 @@ } } - & .ck-widget__type-around__line { + /* + * Styles for the horizontal "fake caret" which is displayed when the user navigates using the keyboard. + */ + & .ck-widget__type-around__fake-caret { display: none; position: absolute; left: 0; right: 0; - - &.ck-widget__type-around__line_before { - top: -1px; - } - - &.ck-widget__type-around__line_after { - bottom: -1px; - } } - &.ck-widget_type-around_active_before > .ck-widget__type-around > .ck-widget__type-around__line_before { + /* + * Styles for the horizontal "fake caret" when it should be displayed before the widget (backward keyboard navigation). + */ + &.ck-widget_type-around_show-fake-caret_before > .ck-widget__type-around > .ck-widget__type-around__fake-caret { + top: -1px; display: block; } - &.ck-widget_type-around_active_after > .ck-widget__type-around > .ck-widget__type-around__line_after { + /* + * Styles for the horizontal "fake caret" when it should be displayed after the widget (forward keyboard navigation). + */ + &.ck-widget_type-around_show-fake-caret_after > .ck-widget__type-around > .ck-widget__type-around__fake-caret { + bottom: -1px; display: block; } } From 33e9b37c3808e53cf1e6025fb7ed250b393d9f4b Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 3 Jun 2020 10:32:35 +0200 Subject: [PATCH 23/69] Tests: Added tests fro the isSafeKeystroke() helper. --- .../utils/injectunsafekeystrokeshandling.js | 15 ++-- .../utils/injectunsafekeystrokeshandling.js | 85 +++++++++++++++++++ 2 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js diff --git a/packages/ckeditor5-typing/src/utils/injectunsafekeystrokeshandling.js b/packages/ckeditor5-typing/src/utils/injectunsafekeystrokeshandling.js index 1ec4e31ab09..31058042ede 100644 --- a/packages/ckeditor5-typing/src/utils/injectunsafekeystrokeshandling.js +++ b/packages/ckeditor5-typing/src/utils/injectunsafekeystrokeshandling.js @@ -154,13 +154,14 @@ for ( let code = 112; code <= 135; code++ ) { safeKeycodes.push( code ); } -// Returns `true` if a keystroke should not cause any content change caused by "typing". -// -// Note: This implementation is very simple and will need to be refined with time. -// -// @private -// @param {engine.view.observer.keyObserver.KeyEventData} keyData -// @returns {Boolean} +/** + * Returns `true` if a keystroke will not result in "typing". + * + * Note: This implementation is very simple and will need to be refined with time. + * + * @param {module:engine/view/observer/keyobserver~KeyEventData} keyData + * @returns {Boolean} + */ export function isSafeKeystroke( keyData ) { // Keystrokes which contain Ctrl don't represent typing. if ( keyData.ctrlKey ) { diff --git a/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js b/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js new file mode 100644 index 00000000000..d7cfc1e5734 --- /dev/null +++ b/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js @@ -0,0 +1,85 @@ +/** + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import { + keyCodes +} from '@ckeditor/ckeditor5-utils/src/keyboard'; +import { isSafeKeystroke } from '../../src/utils/injectunsafekeystrokeshandling'; + +describe( 'unsafe keystroke handling utils', () => { + describe( 'isSafeKeystroke()', () => { + it( 'should return "true" for any keystroke with the Ctrl key', () => { + expect( isSafeKeystroke( { keyCode: keyCodes.a, ctrlKey: true } ), 'Ctrl+a' ).to.be.true; + expect( isSafeKeystroke( { keyCode: keyCodes[ 48 ], ctrlKey: true } ), 'Ctrk+0' ).to.be.true; + } ); + + it( 'should return "true" for all arrow keys', () => { + expect( isSafeKeystroke( { keyCode: keyCodes.arrowup } ), 'arrow up' ).to.be.true; + expect( isSafeKeystroke( { keyCode: keyCodes.arrowdown } ), 'arrow down' ).to.be.true; + expect( isSafeKeystroke( { keyCode: keyCodes.arrowleft } ), 'arrow left' ).to.be.true; + expect( isSafeKeystroke( { keyCode: keyCodes.arrowright } ), 'arrow right' ).to.be.true; + } ); + + it( 'should return "true" for function (Fn) keystrokes', () => { + expect( isSafeKeystroke( { keyCode: 112 } ), 'F1' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 113 } ), 'F2' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 114 } ), 'F3' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 115 } ), 'F4' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 116 } ), 'F5' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 117 } ), 'F6' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 118 } ), 'F7' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 119 } ), 'F8' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 120 } ), 'F9' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 121 } ), 'F10' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 122 } ), 'F11' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 123 } ), 'F12' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 124 } ), 'F13' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 125 } ), 'F14' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 126 } ), 'F15' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 127 } ), 'F16' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 128 } ), 'F17' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 129 } ), 'F18' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 130 } ), 'F19' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 131 } ), 'F20' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 132 } ), 'F21' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 133 } ), 'F22' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 134 } ), 'F23' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 135 } ), 'F24' ).to.be.true; + } ); + + it( 'should return "true" for other safe keystrokes', () => { + expect( isSafeKeystroke( { keyCode: 9 } ), 'Tab' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 16 } ), 'Shift' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 17 } ), 'Ctrl' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 18 } ), 'Alt' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 19 } ), 'Pause' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 20 } ), 'CapsLock' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 27 } ), 'Escape' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 33 } ), 'PageUp' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 34 } ), 'PageDown' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 35 } ), 'Home' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 36 } ), 'End,' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 45 } ), 'Insert' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 91 } ), 'Windows' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 93 } ), 'Menu key' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 144 } ), 'NumLock' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 145 } ), 'ScrollLock' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 173 } ), 'Mute/Unmute' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 174 } ), 'Volume up' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 175 } ), 'Volume down' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 176 } ), 'Next song' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 177 } ), 'Previous song' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 178 } ), 'Stop' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 179 } ), 'Play/Pause' ).to.be.true; + expect( isSafeKeystroke( { keyCode: 255 } ), 'Display brightness (increase and decrease)' ).to.be.true; + } ); + + it( 'should return "false" for the keystrokes that result in typing', () => { + expect( isSafeKeystroke( { keyCode: keyCodes.a } ), 'a' ).to.be.false; + expect( isSafeKeystroke( { keyCode: keyCodes[ 48 ] } ), '0' ).to.be.false; + expect( isSafeKeystroke( { keyCode: keyCodes.a, altKey: true } ), 'Alt+a' ).to.be.false; + } ); + } ); +} ); From 8a3d137a31ff1305674a8aa358603aac71624e74 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 3 Jun 2020 15:53:43 +0200 Subject: [PATCH 24/69] Added the insert paragraph on (Shift+)Enter feature that used to be a part of the Widget plugin to the WidgetTypeAround plugin. --- .../src/widgettypearound/widgettypearound.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 371b8d4f623..50e2b1d5c0f 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -374,7 +374,25 @@ export default class WidgetTypeAround extends Plugin { const editingView = editor.editing.view; this.listenTo( editingView.document, 'enter', ( evt, domEventData ) => { + const selectedViewElement = editingView.document.selection.getSelectedElement(); + const selectedModelElement = editor.editing.mapper.toModelElement( selectedViewElement ); + const schema = editor.model.schema; + let wasHandled; + + // First check if the widget is selected and there's a type around selection attribute associated + // with the "fake caret" that would tell where to insert a new paragraph. if ( this._insertParagraphAccordingToSelectionAttribute() ) { + wasHandled = true; + } + // Then, if there is no selection attribute associated with the "fake caret", check if the widget + // simply is selected and create a new paragraph according to the keystroke (Shift+)Enter. + else if ( isTypeAroundWidget( selectedViewElement, selectedModelElement, schema ) ) { + this._insertParagraph( selectedViewElement, domEventData.isSoft ? 'before' : 'after' ); + + wasHandled = true; + } + + if ( wasHandled ) { domEventData.preventDefault(); evt.stop(); } From 45ca5bafc42c48339e2cf2a32dde752b19677dfc Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 3 Jun 2020 17:39:07 +0200 Subject: [PATCH 25/69] Tests: Added tests for arrow keys and the "fake" type around caret. --- .../src/widgettypearound/widgettypearound.js | 7 +- .../widgettypearound/widgettypearound.js | 449 ++++++++++++++++-- 2 files changed, 410 insertions(+), 46 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 50e2b1d5c0f..52491309c49 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -208,7 +208,12 @@ export default class WidgetTypeAround extends Plugin { // selection as soon as the model range changes. This attribute only makes sense when a widget is selected // (and the "fake horizontal caret" is visible) so whenever the range changes (e.g. selection moved somewhere else), // let's get rid of the attribute so that the selection downcast dispatcher isn't even bothered. - modelSelection.on( 'change:range', () => { + modelSelection.on( 'change:range', ( evt, data ) => { + // Do not reset the selection attribute when the change was indirect. + if ( !data.directChange ) { + return; + } + if ( !modelSelection.hasAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ) { return; } diff --git a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js index 84d52070cad..925fdfaacc2 100644 --- a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js @@ -15,6 +15,7 @@ import WidgetTypeAround from '../../src/widgettypearound/widgettypearound'; import { toWidget } from '../../src/utils'; import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard'; describe( 'WidgetTypeAround', () => { let element, plugin, editor, editingView, viewDocument, viewRoot; @@ -162,6 +163,21 @@ describe( 'WidgetTypeAround', () => { expect( domWrapper.querySelectorAll( '.ck-widget__type-around__button' ) ).to.have.length( 2 ); } ); + it( 'should inject a fake caret into the wrapper', () => { + setModelData( editor.model, '' ); + + const viewWidget = viewRoot.getChild( 0 ); + + expect( viewWidget.getChild( 1 ).is( 'uiElement' ) ).to.be.true; + expect( viewWidget.getChild( 1 ).hasClass( 'ck' ) ).to.be.true; + expect( viewWidget.getChild( 1 ).hasClass( 'ck-reset_all' ) ).to.be.true; + expect( viewWidget.getChild( 1 ).hasClass( 'ck-widget__type-around' ) ).to.be.true; + + const domWrapper = editingView.domConverter.viewToDom( viewWidget.getChild( 1 ) ); + + expect( domWrapper.querySelectorAll( '.ck-widget__type-around__fake-caret' ) ).to.have.length( 1 ); + } ); + describe( 'UI button to type around', () => { let buttonBefore, buttonAfter; @@ -171,8 +187,8 @@ describe( 'WidgetTypeAround', () => { const viewWidget = viewRoot.getChild( 0 ); const domWrapper = editingView.domConverter.viewToDom( viewWidget.getChild( 1 ) ); - buttonBefore = domWrapper.firstChild; - buttonAfter = domWrapper.lastChild; + buttonBefore = domWrapper.children[ 0 ]; + buttonAfter = domWrapper.children[ 1 ]; } ); it( 'should have proper CSS classes', () => { @@ -250,75 +266,418 @@ describe( 'WidgetTypeAround', () => { } ); } ); - describe( 'detection and CSS classes of widgets needing the typing around support', () => { - it( 'should detect widgets that are a first child of the parent container', () => { - setModelData( editor.model, 'foo' ); + describe( 'typing around view widgets using keyboard', () => { + let model, eventInfoStub, domEventDataStub; - const viewWidget = viewRoot.getChild( 0 ); + beforeEach( () => { + model = editor.model; + } ); + + describe( '"fake caret" activation', () => { + it( 'should activate before when the collapsed selection is before a widget and the navigation is forward', () => { + setModelData( editor.model, 'foo[]' ); + + fireKeyboardEvent( 'arrowright' ); + + expect( getModelData( model ) ).to.equal( 'foo[]' ); + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + const viewWidget = viewRoot.getChild( 1 ); + + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.true; + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false; + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should activate after when the collapsed selection is after a widget and the navigation is backward', () => { + setModelData( editor.model, '[]foo' ); + + fireKeyboardEvent( 'arrowleft' ); + + expect( getModelData( model ) ).to.equal( '[]foo' ); + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + + const viewWidget = viewRoot.getChild( 0 ); + + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false; + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.true; + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should activate after when the widget is selected and the navigation is forward', () => { + setModelData( editor.model, '[]' ); + + fireKeyboardEvent( 'arrowright' ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + + const viewWidget = viewRoot.getChild( 0 ); + + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false; + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.true; + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should activate before when the widget is selected and the navigation is backward', () => { + setModelData( editor.model, '[]' ); + + fireKeyboardEvent( 'arrowleft' ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + const viewWidget = viewRoot.getChild( 0 ); + + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.true; + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false; + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should not activate when the selection is before the widget but the non-arrow key was pressed', () => { + setModelData( editor.model, 'fo[]o' ); + + fireKeyboardEvent( 'a' ); + + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + const viewWidget = viewRoot.getChild( 1 ); + + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false; + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false; + + sinon.assert.notCalled( eventInfoStub.stop ); + sinon.assert.notCalled( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should not activate when the selection is not before the widget and navigating forward', () => { + setModelData( editor.model, 'fo[]o' ); + + fireKeyboardEvent( 'arrowright' ); + + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + const viewWidget = viewRoot.getChild( 1 ); + + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false; + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false; + + sinon.assert.notCalled( eventInfoStub.stop ); + sinon.assert.notCalled( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should not activate when the selection is not after the widget and navigating backward', () => { + setModelData( editor.model, 'f[]oo' ); + + fireKeyboardEvent( 'arrowleft' ); + + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + const viewWidget = viewRoot.getChild( 0 ); + + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false; + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false; + + sinon.assert.notCalled( eventInfoStub.stop ); + sinon.assert.notCalled( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should not activate when the non-collapsed selection is before the widget and navigating forward', () => { + setModelData( editor.model, 'fo[o]' ); + + fireKeyboardEvent( 'arrowright' ); + + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + const viewWidget = viewRoot.getChild( 1 ); + + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false; + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false; + + sinon.assert.notCalled( eventInfoStub.stop ); + sinon.assert.notCalled( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should not activate when the non-collapsed selection is after the widget and navigating backward', () => { + setModelData( editor.model, '[f]oo' ); + + fireKeyboardEvent( 'arrowleft' ); + + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + const viewWidget = viewRoot.getChild( 0 ); + + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false; + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false; + + sinon.assert.notCalled( eventInfoStub.stop ); + sinon.assert.notCalled( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should not activate selection downcast when a non–type-around-friendly widget is selected', () => { + setModelData( editor.model, 'foo[]' ); + + model.change( writer => { + // Simply trigger the selection downcast. + writer.setSelectionAttribute( 'foo', 'bar' ); + } ); + + const viewWidget = viewRoot.getChild( 0 ).getChild( 1 ); + + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false; + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false; - assertIsTypeAroundBefore( viewWidget, true ); - assertIsTypeAroundAfter( viewWidget, false ); + sinon.assert.notCalled( eventInfoStub.stop ); + sinon.assert.notCalled( domEventDataStub.domEvent.preventDefault ); + } ); } ); - it( 'should detect widgets that are a last child of the parent container', () => { - setModelData( editor.model, 'foo' ); + describe( '"fake caret" deactivation', () => { + it( 'should deactivate when the widget is selected and the navigation is backward to a valid position', () => { + setModelData( editor.model, 'foo[]' ); - const viewWidget = viewRoot.getChild( 1 ); + fireKeyboardEvent( 'arrowleft' ); + + expect( getModelData( model ) ).to.equal( 'foo[]' ); + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + + fireKeyboardEvent( 'arrowleft' ); + + expect( getModelData( model ) ).to.equal( 'foo[]' ); + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + const viewWidget = viewRoot.getChild( 1 ); + + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false; + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false; + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should deactivate when the widget is selected and the navigation is forward to a valid position', () => { + setModelData( editor.model, '[]foo' ); + + fireKeyboardEvent( 'arrowright' ); + + expect( getModelData( model ) ).to.equal( '[]foo' ); + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + + fireKeyboardEvent( 'arrowright' ); + + expect( getModelData( model ) ).to.equal( '[]foo' ); + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + const viewWidget = viewRoot.getChild( 0 ); + + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false; + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false; + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should not deactivate when the widget is selected and the navigation is backward but there is nowhere to go', () => { + setModelData( editor.model, '[]' ); + + fireKeyboardEvent( 'arrowleft' ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + + fireKeyboardEvent( 'arrowleft' ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + + const viewWidget = viewRoot.getChild( 0 ); + + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.true; + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false; + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should not deactivate when the widget is selected and the navigation is forward but there is nowhere to go', () => { + setModelData( editor.model, '[]' ); + + fireKeyboardEvent( 'arrowright' ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + + fireKeyboardEvent( 'arrowright' ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + + const viewWidget = viewRoot.getChild( 0 ); + + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false; + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.true; + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should deactivate when the widget is selected and the navigation is against the fake caret (backward)', () => { + setModelData( editor.model, '[]' ); + + fireKeyboardEvent( 'arrowleft' ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + + fireKeyboardEvent( 'arrowright' ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + const viewWidget = viewRoot.getChild( 0 ); + + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false; + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false; + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should deactivate when the widget is selected and the navigation is against the fake caret (forward)', () => { + setModelData( editor.model, '[]' ); + + fireKeyboardEvent( 'arrowright' ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + + fireKeyboardEvent( 'arrowleft' ); - assertIsTypeAroundBefore( viewWidget, false ); - assertIsTypeAroundAfter( viewWidget, true ); + expect( getModelData( model ) ).to.equal( '[]' ); + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + const viewWidget = viewRoot.getChild( 0 ); + + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false; + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false; + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); } ); - it( 'should not detect widgets that are surrounded by sibling which allow the selection', () => { - setModelData( editor.model, 'foobar' ); + it( 'should quit the "fake caret" mode when the editor loses focus', () => { + editor.ui.focusTracker.isFocused = true; + + setModelData( editor.model, 'foo[]' ); + + fireKeyboardEvent( 'arrowright' ); + + expect( getModelData( model ) ).to.equal( 'foo[]' ); + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + editor.ui.focusTracker.isFocused = false; const viewWidget = viewRoot.getChild( 1 ); - assertIsTypeAroundBefore( viewWidget, false ); - assertIsTypeAroundAfter( viewWidget, false ); + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false; + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false; } ); - it( 'should detect widgets that have another block widget as a next sibling', () => { - setModelData( editor.model, '' ); + it( 'should quit the "fake caret" mode when the user changed the selection', () => { + setModelData( editor.model, 'foo[]' ); - const firstViewWidget = viewRoot.getChild( 0 ); + fireKeyboardEvent( 'arrowright' ); - assertIsTypeAroundBefore( firstViewWidget, true ); - assertIsTypeAroundAfter( firstViewWidget, true ); - } ); + expect( getModelData( model ) ).to.equal( 'foo[]' ); + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); - it( 'should detect widgets that have another block widget as a previous sibling', () => { - setModelData( editor.model, '' ); + model.change( writer => { + writer.setSelection( model.document.getRoot().getChild( 0 ), 'in' ); + } ); - const lastViewWidget = viewRoot.getChild( 1 ); + const viewWidget = viewRoot.getChild( 1 ); - assertIsTypeAroundBefore( lastViewWidget, true ); - assertIsTypeAroundAfter( lastViewWidget, true ); + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false; + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false; } ); - it( 'should not detect inline widgets even if they fall in previous categories', () => { - setModelData( editor.model, - '' - ); + it( 'should not quit the "fake caret" mode when the selection changed as a result of an indirect change', () => { + setModelData( editor.model, 'foo[]' ); + + fireKeyboardEvent( 'arrowright' ); + + expect( getModelData( model ) ).to.equal( 'foo[]' ); + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); - const firstViewWidget = viewRoot.getChild( 0 ).getChild( 0 ); - const lastViewWidget = viewRoot.getChild( 0 ).getChild( 1 ); + // This could happen in collaboration. + model.document.selection.fire( 'change:range', { + directChange: false + } ); + + expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); - assertIsTypeAroundBefore( firstViewWidget, false ); - assertIsTypeAroundAfter( firstViewWidget, false ); + const viewWidget = viewRoot.getChild( 1 ); - assertIsTypeAroundBefore( lastViewWidget, false ); - assertIsTypeAroundAfter( lastViewWidget, false ); + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.true; + expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false; } ); - } ); - function assertIsTypeAroundBefore( viewWidget, expected ) { - expect( viewWidget.hasClass( 'ck-widget_can-type-around_before' ) ).to.equal( expected ); - } + function getDomEvent() { + return { + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + } - function assertIsTypeAroundAfter( viewWidget, expected ) { - expect( viewWidget.hasClass( 'ck-widget_can-type-around_after' ) ).to.equal( expected ); - } + function fireKeyboardEvent( key ) { + eventInfoStub = new EventInfo( viewDocument, 'keydown' ); + + sinon.spy( eventInfoStub, 'stop' ); + + domEventDataStub = new DomEventData( viewDocument, getDomEvent(), { + document: viewDocument, + domTarget: editingView.getDomRoot(), + keyCode: getCode( key ) + } ); + + viewDocument.fire( eventInfoStub, domEventDataStub ); + } + } ); function blockWidgetPlugin( editor ) { editor.model.schema.register( 'blockWidget', { From b7afa0627b644d3f659013b6c37076117eb4ab9e Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 4 Jun 2020 12:56:27 +0200 Subject: [PATCH 26/69] Tests: Added tests for various aspects of inserting a paragraph around a widget using the keyboard. --- .../widgettypearound/widgettypearound.js | 244 +++++++++++++++--- 1 file changed, 212 insertions(+), 32 deletions(-) diff --git a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js index 925fdfaacc2..36a5e5a02c1 100644 --- a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js @@ -9,6 +9,7 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata'; import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; +import ViewText from '@ckeditor/ckeditor5-engine/src/view/text'; import Widget from '../../src/widget'; import WidgetTypeAround from '../../src/widgettypearound/widgettypearound'; @@ -226,6 +227,20 @@ describe( 'WidgetTypeAround', () => { sinon.assert.calledOnce( preventDefaultSpy ); sinon.assert.calledOnce( stopSpy ); } ); + + it( 'should not cause WidgetTypeAround#_insertParagraph() when clicked something other than the button', () => { + const typeAroundSpy = sinon.spy( plugin, '_insertParagraph' ); + + const eventInfo = new EventInfo( viewDocument, 'mousedown' ); + const domEventDataMock = new DomEventData( editingView, { + // Clicking a widget. + target: editingView.domConverter.viewToDom( viewRoot.getChild( 0 ) ), + preventDefault: sinon.spy() + } ); + + viewDocument.fire( eventInfo, domEventDataMock ); + sinon.assert.notCalled( typeAroundSpy ); + } ); } ); describe( 'button to type "after" a widget', () => { @@ -267,10 +282,11 @@ describe( 'WidgetTypeAround', () => { } ); describe( 'typing around view widgets using keyboard', () => { - let model, eventInfoStub, domEventDataStub; + let model, modelSelection, eventInfoStub, domEventDataStub; beforeEach( () => { model = editor.model; + modelSelection = model.document.selection; } ); describe( '"fake caret" activation', () => { @@ -280,7 +296,7 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowright' ); expect( getModelData( model ) ).to.equal( 'foo[]' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); const viewWidget = viewRoot.getChild( 1 ); @@ -297,7 +313,7 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowleft' ); expect( getModelData( model ) ).to.equal( '[]foo' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); const viewWidget = viewRoot.getChild( 0 ); @@ -314,7 +330,7 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowright' ); expect( getModelData( model ) ).to.equal( '[]' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); const viewWidget = viewRoot.getChild( 0 ); @@ -331,7 +347,7 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowleft' ); expect( getModelData( model ) ).to.equal( '[]' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); const viewWidget = viewRoot.getChild( 0 ); @@ -343,11 +359,13 @@ describe( 'WidgetTypeAround', () => { } ); it( 'should not activate when the selection is before the widget but the non-arrow key was pressed', () => { - setModelData( editor.model, 'fo[]o' ); + setModelData( editor.model, 'foo[]' ); fireKeyboardEvent( 'a' ); + fireMutation( 'a' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + expect( getModelData( model ) ).to.equal( 'fooa[]' ); const viewWidget = viewRoot.getChild( 1 ); @@ -363,7 +381,7 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowright' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; const viewWidget = viewRoot.getChild( 1 ); @@ -379,7 +397,7 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowleft' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; const viewWidget = viewRoot.getChild( 0 ); @@ -395,7 +413,7 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowright' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; const viewWidget = viewRoot.getChild( 1 ); @@ -411,7 +429,7 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowleft' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; const viewWidget = viewRoot.getChild( 0 ); @@ -447,7 +465,7 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowleft' ); expect( getModelData( model ) ).to.equal( 'foo[]' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); sinon.assert.calledOnce( eventInfoStub.stop ); sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); @@ -455,7 +473,7 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowleft' ); expect( getModelData( model ) ).to.equal( 'foo[]' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; const viewWidget = viewRoot.getChild( 1 ); @@ -472,7 +490,7 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowright' ); expect( getModelData( model ) ).to.equal( '[]foo' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); sinon.assert.calledOnce( eventInfoStub.stop ); sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); @@ -480,7 +498,7 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowright' ); expect( getModelData( model ) ).to.equal( '[]foo' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; const viewWidget = viewRoot.getChild( 0 ); @@ -497,7 +515,7 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowleft' ); expect( getModelData( model ) ).to.equal( '[]' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); sinon.assert.calledOnce( eventInfoStub.stop ); sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); @@ -505,7 +523,7 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowleft' ); expect( getModelData( model ) ).to.equal( '[]' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); sinon.assert.calledOnce( eventInfoStub.stop ); sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); @@ -525,7 +543,7 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowright' ); expect( getModelData( model ) ).to.equal( '[]' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); sinon.assert.calledOnce( eventInfoStub.stop ); sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); @@ -533,7 +551,7 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowright' ); expect( getModelData( model ) ).to.equal( '[]' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); sinon.assert.calledOnce( eventInfoStub.stop ); sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); @@ -553,7 +571,7 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowleft' ); expect( getModelData( model ) ).to.equal( '[]' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); sinon.assert.calledOnce( eventInfoStub.stop ); sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); @@ -561,7 +579,7 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowright' ); expect( getModelData( model ) ).to.equal( '[]' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; const viewWidget = viewRoot.getChild( 0 ); @@ -578,7 +596,7 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowright' ); expect( getModelData( model ) ).to.equal( '[]' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); sinon.assert.calledOnce( eventInfoStub.stop ); sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); @@ -586,7 +604,7 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowleft' ); expect( getModelData( model ) ).to.equal( '[]' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; const viewWidget = viewRoot.getChild( 0 ); @@ -606,13 +624,13 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowright' ); expect( getModelData( model ) ).to.equal( 'foo[]' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); editor.ui.focusTracker.isFocused = false; const viewWidget = viewRoot.getChild( 1 ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false; expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false; } ); @@ -623,7 +641,7 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowright' ); expect( getModelData( model ) ).to.equal( 'foo[]' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); model.change( writer => { writer.setSelection( model.document.getRoot().getChild( 0 ), 'in' ); @@ -631,7 +649,7 @@ describe( 'WidgetTypeAround', () => { const viewWidget = viewRoot.getChild( 1 ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false; expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false; } ); @@ -642,14 +660,14 @@ describe( 'WidgetTypeAround', () => { fireKeyboardEvent( 'arrowright' ); expect( getModelData( model ) ).to.equal( 'foo[]' ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); // This could happen in collaboration. model.document.selection.fire( 'change:range', { directChange: false } ); - expect( model.document.selection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); const viewWidget = viewRoot.getChild( 1 ); @@ -657,6 +675,151 @@ describe( 'WidgetTypeAround', () => { expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false; } ); + describe( 'inserting a new paragraph', () => { + describe( 'on Enter key press when the "fake caret" is activated', () => { + it( 'should insert a paragraph before a widget if the caret was "before" it', () => { + setModelData( editor.model, '[]' ); + + fireKeyboardEvent( 'arrowleft' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + fireKeyboardEvent( 'enter' ); + expect( getModelData( model ) ).to.equal( '[]' ); + } ); + + it( 'should insert a paragraph after a widget if the caret was "after" it', () => { + setModelData( editor.model, '[]' ); + + fireKeyboardEvent( 'arrowright' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + + fireKeyboardEvent( 'enter' ); + expect( getModelData( model ) ).to.equal( '[]' ); + } ); + + it( 'should integrate with the undo feature', () => { + setModelData( editor.model, '[]' ); + + fireKeyboardEvent( 'arrowleft' ); + fireKeyboardEvent( 'enter' ); + + expect( getModelData( model ) ).to.equal( '[]' ); + + editor.execute( 'undo' ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + } ); + } ); + + describe( 'on Enter key press when the widget is selected (no "fake caret", though)', () => { + it( 'should insert a new paragraph after the widget if Enter was pressed', () => { + setModelData( editor.model, '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + fireKeyboardEvent( 'enter' ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + } ); + + it( 'should insert a new paragraph before the widget if Shift+Enter was pressed', () => { + setModelData( editor.model, '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + fireKeyboardEvent( 'enter', { shiftKey: true } ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + } ); + + it( 'should integrate with the undo feature', () => { + setModelData( editor.model, '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + fireKeyboardEvent( 'enter' ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + editor.execute( 'undo' ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + } ); + + it( 'should do nothing if a non-type-around-friendly content is selected', () => { + setModelData( editor.model, 'foo[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + fireKeyboardEvent( 'enter' ); + + expect( getModelData( model ) ).to.equal( 'foo[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + } ); + } ); + + describe( 'on typing an "unsafe" character when the "fake caret" is activated ', () => { + it( 'should insert a character inside a new paragraph before a widget if the caret was "before" it', () => { + setModelData( editor.model, '[]' ); + + fireKeyboardEvent( 'arrowleft' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + fireKeyboardEvent( 'a' ); + fireMutation( 'a' ); + + expect( getModelData( model ) ).to.equal( 'a[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + } ); + + it( 'should insert a character inside a new paragraph after a widget if the caret was "after" it', () => { + setModelData( editor.model, '[]' ); + + fireKeyboardEvent( 'arrowright' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + + fireKeyboardEvent( 'a' ); + fireMutation( 'a' ); + + expect( getModelData( model ) ).to.equal( 'a[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + } ); + + it( 'should do nothing if a "safe" keystroke was pressed', () => { + setModelData( editor.model, '[]' ); + + fireKeyboardEvent( 'arrowright' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + + fireKeyboardEvent( 'esc' ); + fireKeyboardEvent( 'tab' ); + fireKeyboardEvent( 'd', { ctrlKey: true } ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + } ); + + it( 'should integrate with the undo feature', () => { + setModelData( editor.model, '[]' ); + + fireKeyboardEvent( 'arrowleft' ); + fireKeyboardEvent( 'a' ); + fireMutation( 'a' ); + + expect( getModelData( model ) ).to.equal( 'a[]' ); + + editor.execute( 'undo' ); + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + editor.execute( 'undo' ); + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + } ); + } ); + } ); + function getDomEvent() { return { preventDefault: sinon.spy(), @@ -664,19 +827,36 @@ describe( 'WidgetTypeAround', () => { }; } - function fireKeyboardEvent( key ) { + function fireKeyboardEvent( key, modifiers ) { eventInfoStub = new EventInfo( viewDocument, 'keydown' ); sinon.spy( eventInfoStub, 'stop' ); - domEventDataStub = new DomEventData( viewDocument, getDomEvent(), { + const data = { document: viewDocument, domTarget: editingView.getDomRoot(), keyCode: getCode( key ) - } ); + }; + + Object.assign( data, modifiers ); + + domEventDataStub = new DomEventData( viewDocument, getDomEvent(), data ); viewDocument.fire( eventInfoStub, domEventDataStub ); } + + function fireMutation( text ) { + const placeOfMutation = viewDocument.selection.getFirstRange().start; + + viewDocument.fire( 'mutations', [ + { + type: 'children', + oldChildren: [], + newChildren: [ new ViewText( viewDocument, text ) ], + node: placeOfMutation + } + ] ); + } } ); function blockWidgetPlugin( editor ) { From 95cab0f6ebf847b97db816d65143f31d0f460575 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 4 Jun 2020 17:04:36 +0200 Subject: [PATCH 27/69] The InsertParagraphCommand should split position ancestors to find a place where a paragraph is allowed. --- .../src/insertparagraphcommand.js | 23 ++++++++++--- .../tests/insertparagraphcommand.js | 34 +++++++++++++++---- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/packages/ckeditor5-paragraph/src/insertparagraphcommand.js b/packages/ckeditor5-paragraph/src/insertparagraphcommand.js index a7290da4c2d..688f962ec70 100644 --- a/packages/ckeditor5-paragraph/src/insertparagraphcommand.js +++ b/packages/ckeditor5-paragraph/src/insertparagraphcommand.js @@ -18,6 +18,10 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; * position: editor.model.createPositionBefore( element ) * } ); * + * If a paragraph is disallowed in the context of the specific position, the command + * will attempt to split position ancestors to find a place where it is possible + * to insert a paragraph. + * * **Note**: This command moves the selection to the inserted paragraph. * * @extends module:core/command~Command @@ -33,15 +37,24 @@ export default class InsertParagraphCommand extends Command { */ execute( options ) { const model = this.editor.model; - - if ( !model.schema.checkChild( options.position, 'paragraph' ) ) { - return; - } + let position = options.position; model.change( writer => { const paragraph = writer.createElement( 'paragraph' ); - model.insertContent( paragraph, options.position ); + if ( !model.schema.checkChild( position.parent, paragraph ) ) { + const allowedParent = model.schema.findAllowedParent( position, paragraph ); + + // It could be there's no ancestor limit that would allow paragraph. + // In theory, "paragraph" could be disallowed even in the "$root". + if ( !allowedParent ) { + return; + } + + position = writer.split( position, allowedParent ).position; + } + + model.insertContent( paragraph, position ); writer.setSelection( paragraph, 'in' ); } ); diff --git a/packages/ckeditor5-paragraph/tests/insertparagraphcommand.js b/packages/ckeditor5-paragraph/tests/insertparagraphcommand.js index 290c0e2b029..0463082e964 100644 --- a/packages/ckeditor5-paragraph/tests/insertparagraphcommand.js +++ b/packages/ckeditor5-paragraph/tests/insertparagraphcommand.js @@ -23,7 +23,9 @@ describe( 'InsertParagraphCommand', () => { editor.commands.add( 'insertParagraph', command ); schema.register( 'paragraph', { inheritAllFrom: '$block' } ); schema.register( 'heading1', { inheritAllFrom: '$block', allowIn: 'headersOnly' } ); - schema.register( 'headersOnly', { inheritAllFrom: '$block' } ); + schema.register( 'allowP', { inheritAllFrom: '$block' } ); + schema.register( 'disallowP', { inheritAllFrom: '$block', allowIn: [ 'allowP' ] } ); + model.schema.extend( 'paragraph', { allowIn: [ 'allowP' ] } ); } ); } ); @@ -42,18 +44,38 @@ describe( 'InsertParagraphCommand', () => { expect( getData( model ) ).to.equal( '[]foo' ); } ); - it( 'should do nothing if the paragraph is not allowed at the provided position', () => { - setData( model, 'foo[]' ); + it( 'should split ancestors down to a limit where a paragraph is allowed', () => { + setData( model, 'foo' ); command.execute( { - position: model.createPositionBefore( root.getChild( 0 ).getChild( 0 ) ) + // fo[]o + position: model.createPositionAt( root.getChild( 0 ).getChild( 0 ), 2 ) } ); + expect( getData( model ) ).to.equal( + '' + + 'fo' + + '[]' + + 'o' + + '' + ); + } ); + + it( 'should do nothing if the paragraph is not allowed at the provided position', () => { + // Create a situation where "paragraph" is disallowed even in the "root". + schema.addChildCheck( ( context, childDefinition ) => { + if ( context.endsWith( '$root' ) && childDefinition.name == 'paragraph' ) { + return false; + } + } ); + + setData( model, 'foo[]' ); + command.execute( { - position: model.createPositionAfter( root.getChild( 0 ).getChild( 0 ) ) + position: model.createPositionBefore( root.getChild( 0 ) ) } ); - expect( getData( model ) ).to.equal( 'foo[]' ); + expect( getData( model ) ).to.equal( 'foo[]' ); } ); describe( 'interation with existing paragraphs in the content', () => { From e091c9a9128aaa4935bf3f996b1efe752192d98a Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 4 Jun 2020 17:05:10 +0200 Subject: [PATCH 28/69] Tests: Moved some enter key integration tests from the Widget plugin tests. --- .../widgettypearound/widgettypearound.js | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js index 36a5e5a02c1..ce574bec125 100644 --- a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js @@ -733,6 +733,61 @@ describe( 'WidgetTypeAround', () => { expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; } ); + it( 'should insert a new paragraph only if an entire widget is selected (selected nested editable content)', () => { + setModelData( editor.model, '[foo] bar' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + fireKeyboardEvent( 'enter' ); + + expect( getModelData( model ) ).to.equal( '[] bar' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + } ); + + it( 'should insert a new paragraph only if an entire widget is selected (selected widget siblings)', () => { + setModelData( editor.model, 'f[ooo]o' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + fireKeyboardEvent( 'enter' ); + + expect( getModelData( model ) ).to.equal( 'f[]o' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + } ); + + it( 'should split ancestors to find a place that allows a widget', () => { + model.schema.register( 'allowP', { + inheritAllFrom: '$block' + } ); + model.schema.register( 'disallowP', { + inheritAllFrom: '$block', + allowIn: [ 'allowP' ] + } ); + model.schema.extend( 'blockWidget', { + allowIn: [ 'allowP', 'disallowP' ] + } ); + model.schema.extend( 'paragraph', { + allowIn: [ 'allowP' ] + } ); + + editor.conversion.for( 'downcast' ).elementToElement( { model: 'allowP', view: 'allowP' } ); + editor.conversion.for( 'downcast' ).elementToElement( { model: 'disallowP', view: 'disallowP' } ); + + setModelData( model, + '' + + '[]' + + '' + ); + + fireKeyboardEvent( 'enter' ); + + expect( getModelData( model ) ).to.equal( + '' + + '' + + '[]' + + '' + + '' + ); + } ); + it( 'should integrate with the undo feature', () => { setModelData( editor.model, '[]' ); expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; @@ -865,6 +920,15 @@ describe( 'WidgetTypeAround', () => { isObject: true } ); + editor.model.schema.register( 'nested', { + allowIn: 'blockWidget', + isLimit: true + } ); + + editor.model.schema.extend( '$text', { + allowIn: [ 'nested' ] + } ); + editor.conversion.for( 'downcast' ) .elementToElement( { model: 'blockWidget', @@ -878,6 +942,10 @@ describe( 'WidgetTypeAround', () => { label: 'block widget' } ); } + } ) + .elementToElement( { + model: 'nested', + view: ( modelItem, viewWriter ) => viewWriter.createEditableElement( 'nested', { contenteditable: true } ) } ); } From 8472e700e99028438e1296b0d5a4c77e0a6e5e8e Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 4 Jun 2020 17:05:59 +0200 Subject: [PATCH 29/69] Tests: Updated widget integration tests after obsolete CSS classes related to the WidgetTypeAround plugin have been removed. --- packages/ckeditor5-widget/tests/widget-integration.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-widget/tests/widget-integration.js b/packages/ckeditor5-widget/tests/widget-integration.js index 7a04d5fde3b..7e73f093897 100644 --- a/packages/ckeditor5-widget/tests/widget-integration.js +++ b/packages/ckeditor5-widget/tests/widget-integration.js @@ -113,7 +113,7 @@ describe( 'Widget - integration', () => { expect( getViewData( view ) ).to.equal( '

[]

' + - '
' + + '
' + '
foo bar
' + '
' + '
' @@ -139,7 +139,7 @@ describe( 'Widget - integration', () => { sinon.assert.called( preventDefault ); expect( getViewData( view ) ).to.equal( - '
' + + '
' + '
{foo bar}
' + '
' + '
' @@ -164,7 +164,7 @@ describe( 'Widget - integration', () => { sinon.assert.called( preventDefault ); expect( getViewData( view ) ).to.equal( - '
' + + '
' + '
foo
' + '
{bar}
' + '
' + @@ -191,7 +191,7 @@ describe( 'Widget - integration', () => { sinon.assert.called( preventDefault ); expect( getViewData( view ) ).to.equal( - '
' + + '
' + '
{foo bar}
' + '
' + '
' @@ -243,7 +243,7 @@ describe( 'Widget - integration', () => { expect( getViewData( view ) ).to.equal( '

[]

' + - '
' + + '
' + '
foo bar
' + '
' + '
' From 9ef42859c7a25a722a64c5098743f364431c89c8 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 4 Jun 2020 17:07:05 +0200 Subject: [PATCH 30/69] Tests: Updated Widget tests to consider WidgetTypeAround keyboard support. Removed obsolete WidgetTypeAround CSS classes. Moved enter integration tests to the WidgetTypeAround tests. --- packages/ckeditor5-widget/tests/widget.js | 319 ++++++++++------------ 1 file changed, 149 insertions(+), 170 deletions(-) diff --git a/packages/ckeditor5-widget/tests/widget.js b/packages/ckeditor5-widget/tests/widget.js index 5d3b9256f93..c07b69d1aa5 100644 --- a/packages/ckeditor5-widget/tests/widget.js +++ b/packages/ckeditor5-widget/tests/widget.js @@ -6,6 +6,7 @@ /* global document */ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import Enter from '@ckeditor/ckeditor5-enter/src/enter'; import Widget from '../src/widget'; import WidgetTypeAround from '../src/widgettypearound/widgettypearound'; import Typing from '@ckeditor/ckeditor5-typing/src/typing'; @@ -28,7 +29,7 @@ describe( 'Widget', () => { return ClassicTestEditor .create( element, { - plugins: [ Widget, Typing ] + plugins: [ Widget, Typing, Enter ] } ) .then( newEditor => { editor = newEditor; @@ -198,7 +199,7 @@ describe( 'Widget', () => { setModelData( model, '[foo bar]' ); expect( getViewData( view ) ).to.equal( - '[
' + 'foo bar' + '' + @@ -221,11 +222,11 @@ describe( 'Widget', () => { expect( getViewData( view ) ).to.equal( '

{foo

' + - '
' + + '
' + '' + '
' + '
' + - '
' + '' + '
' + @@ -244,7 +245,7 @@ describe( 'Widget', () => { expect( getViewData( view ) ).to.equal( '

foo

' + - '[
' + + '[
' + 'foo' + '' + '
' + @@ -257,7 +258,7 @@ describe( 'Widget', () => { expect( getViewData( view ) ).to.equal( '

{}foo

' + - '
' + + '
' + 'foo' + '' + '
' + @@ -269,7 +270,7 @@ describe( 'Widget', () => { setModelData( model, 'foo bar[baz]' ); expect( getViewData( view ) ).to.equal( - '
' + + '
' + '
foo bar
' + '' + '
' + @@ -283,112 +284,128 @@ describe( 'Widget', () => { test( 'should move selection forward from selected object - right arrow', '[]foo', - keyCodes.arrowright, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ keyCodes.arrowright, keyCodes.arrowright ], '[]foo' ); test( 'should move selection forward from selected object - down arrow', '[]foo', - keyCodes.arrowdown, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ keyCodes.arrowdown, keyCodes.arrowdown ], '[]foo' ); test( 'should move selection backward from selected object - left arrow', 'foo[]', - keyCodes.arrowleft, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ keyCodes.arrowleft, keyCodes.arrowleft ], 'foo[]' ); test( 'should move selection backward from selected object - up arrow', 'foo[]', - keyCodes.arrowup, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ keyCodes.arrowup, keyCodes.arrowup ], 'foo[]' ); test( 'should move selection to next widget - right arrow', '[]', - keyCodes.arrowright, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ keyCodes.arrowright, keyCodes.arrowright ], '[]' ); test( 'should move selection to next widget - down arrow', '[]', - keyCodes.arrowdown, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ keyCodes.arrowdown, keyCodes.arrowdown ], '[]' ); test( 'should move selection to previous widget - left arrow', '[]', - keyCodes.arrowleft, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ keyCodes.arrowleft, keyCodes.arrowleft ], '[]' ); test( 'should move selection to previous widget - up arrow', '[]', - keyCodes.arrowup, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ keyCodes.arrowup, keyCodes.arrowup ], '[]' ); + // Note: Testing an inline widget only because block widgets are handled and tested by the WidgetTypeAround plugin. test( - 'should do nothing on non-collapsed selection next to object - right arrow', - 'ba[r]', + 'should do nothing on non-collapsed selection next to an inline widget - right arrow', + 'ba[r]', keyCodes.arrowright, - 'ba[r]' + 'ba[r]' ); + // Note: Testing an inline widget only because block widgets are handled and tested by the WidgetTypeAround plugin. test( - 'should do nothing on non-collapsed selection next to object - down arrow', - 'ba[r]', + 'should do nothing on non-collapsed selection next to an inline widget - down arrow', + 'ba[r]', keyCodes.arrowdown, - 'ba[r]' + 'ba[r]' ); + // Note: Testing an inline widget only because block widgets are handled and tested by the WidgetTypeAround plugin. test( - 'should do nothing on non-collapsed selection next to object - left arrow', - '[b]ar', + 'should do nothing on non-collapsed selection next to an inline widget - left arrow', + '[b]ar', keyCodes.arrowleft, - '[b]ar' + '[b]ar' ); + // Note: Testing an inline widget only because block widgets are handled and tested by the WidgetTypeAround plugin. test( - 'should do nothing on non-collapsed selection next to object - up arrow', - '[b]ar', + 'should do nothing on non-collapsed selection next to an inline widget - up arrow', + '[b]ar', keyCodes.arrowup, - '[b]ar' + '[b]ar' ); test( 'should not move selection if there is no correct location - right arrow', 'foo[]', - keyCodes.arrowright, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ keyCodes.arrowright, keyCodes.arrowright ], 'foo[]' ); test( 'should not move selection if there is no correct location - down arrow', 'foo[]', - keyCodes.arrowdown, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ keyCodes.arrowdown, keyCodes.arrowdown ], 'foo[]' ); test( 'should not move selection if there is no correct location - left arrow', '[]foo', - keyCodes.arrowleft, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ keyCodes.arrowleft, keyCodes.arrowleft ], '[]foo' ); test( 'should not move selection if there is no correct location - up arrow', '[]foo', - keyCodes.arrowup, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ keyCodes.arrowup, keyCodes.arrowup ], '[]foo' ); @@ -409,10 +426,12 @@ describe( 'Widget', () => { setModelData( model, 'foo[]' ); viewDocument.on( 'keydown', keydownHandler ); + // Note: The first step is handled by the WidgetTypeAround plugin. + viewDocument.fire( 'keydown', domEventDataMock ); viewDocument.fire( 'keydown', domEventDataMock ); expect( getModelData( model ) ).to.equal( 'foo[]' ); - sinon.assert.calledOnce( domEventDataMock.preventDefault ); + sinon.assert.calledTwice( domEventDataMock.preventDefault ); sinon.assert.notCalled( keydownHandler ); } ); @@ -425,10 +444,12 @@ describe( 'Widget', () => { setModelData( model, '[]foo' ); viewDocument.on( 'keydown', keydownHandler ); + // Note: The first step is handled by the WidgetTypeAround plugin. + viewDocument.fire( 'keydown', domEventDataMock ); viewDocument.fire( 'keydown', domEventDataMock ); expect( getModelData( model ) ).to.equal( '[]foo' ); - sinon.assert.calledOnce( domEventDataMock.preventDefault ); + sinon.assert.calledTwice( domEventDataMock.preventDefault ); sinon.assert.notCalled( keydownHandler ); } ); @@ -491,84 +512,132 @@ describe( 'Widget', () => { test( 'should work correctly with modifier key: right arrow + ctrl', '[]foo', - { keyCode: keyCodes.arrowright, ctrlKey: true }, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ + { keyCode: keyCodes.arrowright, ctrlKey: true }, + { keyCode: keyCodes.arrowright, ctrlKey: true } + ], '[]foo' ); test( 'should work correctly with modifier key: right arrow + alt', '[]foo', - { keyCode: keyCodes.arrowright, altKey: true }, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ + { keyCode: keyCodes.arrowright, altKey: true }, + { keyCode: keyCodes.arrowright, altKey: true } + ], '[]foo' ); test( 'should work correctly with modifier key: right arrow + shift', '[]foo', - { keyCode: keyCodes.arrowright, shiftKey: true }, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ + { keyCode: keyCodes.arrowright, shiftKey: true }, + { keyCode: keyCodes.arrowright, shiftKey: true } + ], '[]foo' ); test( 'should work correctly with modifier key: down arrow + ctrl', '[]foo', - { keyCode: keyCodes.arrowdown, ctrlKey: true }, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ + { keyCode: keyCodes.arrowdown, ctrlKey: true }, + { keyCode: keyCodes.arrowdown, ctrlKey: true } + ], '[]foo' ); test( 'should work correctly with modifier key: down arrow + alt', '[]foo', - { keyCode: keyCodes.arrowdown, altKey: true }, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ + { keyCode: keyCodes.arrowdown, altKey: true }, + { keyCode: keyCodes.arrowdown, altKey: true } + ], '[]foo' ); test( 'should work correctly with modifier key: down arrow + shift', '[]foo', - { keyCode: keyCodes.arrowdown, shiftKey: true }, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ + { keyCode: keyCodes.arrowdown, shiftKey: true }, + { keyCode: keyCodes.arrowdown, shiftKey: true } + ], '[]foo' ); test( 'should work correctly with modifier key: left arrow + ctrl', 'foo[]', - { keyCode: keyCodes.arrowleft, ctrlKey: true }, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ + { keyCode: keyCodes.arrowleft, ctrlKey: true }, + { keyCode: keyCodes.arrowleft, ctrlKey: true } + ], 'foo[]' ); test( 'should work correctly with modifier key: left arrow + alt', 'foo[]', - { keyCode: keyCodes.arrowleft, altKey: true }, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ + { keyCode: keyCodes.arrowleft, altKey: true }, + { keyCode: keyCodes.arrowleft, altKey: true } + ], 'foo[]' ); test( 'should work correctly with modifier key: left arrow + shift', 'foo[]', - { keyCode: keyCodes.arrowleft, shiftKey: true }, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ + { keyCode: keyCodes.arrowleft, shiftKey: true }, + { keyCode: keyCodes.arrowleft, shiftKey: true } + ], 'foo[]' ); test( 'should work correctly with modifier key: up arrow + ctrl', 'foo[]', - { keyCode: keyCodes.arrowup, ctrlKey: true }, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ + { keyCode: keyCodes.arrowup, ctrlKey: true }, + { keyCode: keyCodes.arrowup, ctrlKey: true } + ], 'foo[]' ); test( 'should work correctly with modifier key: up arrow + alt', 'foo[]', - { keyCode: keyCodes.arrowup, altKey: true }, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ + { keyCode: keyCodes.arrowup, altKey: true }, + { keyCode: keyCodes.arrowup, altKey: true } + ], 'foo[]' ); test( 'should work correctly with modifier key: up arrow + shift', 'foo[]', - { keyCode: keyCodes.arrowup, shiftKey: true }, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ + { keyCode: keyCodes.arrowup, shiftKey: true }, + { keyCode: keyCodes.arrowup, shiftKey: true } + ], 'foo[]' ); @@ -695,7 +764,8 @@ describe( 'Widget', () => { test( 'should move selection forward from selected object - left arrow', '[]foo', - keyCodes.arrowleft, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ keyCodes.arrowleft, keyCodes.arrowleft ], '[]foo', null, 'rtl' @@ -704,7 +774,8 @@ describe( 'Widget', () => { test( 'should move selection backward from selected object - right arrow', 'foo[]', - keyCodes.arrowright, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ keyCodes.arrowright, keyCodes.arrowright ], 'foo[]', null, 'rtl' @@ -713,7 +784,8 @@ describe( 'Widget', () => { test( 'should move selection to next widget - left arrow', '[]', - keyCodes.arrowleft, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ keyCodes.arrowleft, keyCodes.arrowleft ], '[]', null, 'rtl' @@ -722,7 +794,8 @@ describe( 'Widget', () => { test( 'should move selection to previous widget - right arrow', '[]', - keyCodes.arrowright, + // Note: The first step is handled by the WidgetTypeAround plugin. + [ keyCodes.arrowright, keyCodes.arrowright ], '[]', null, 'rtl' @@ -730,118 +803,33 @@ describe( 'Widget', () => { } ); } ); - describe( 'enter', () => { - test( - 'should insert a paragraph after the selected widget upon Enter', - '[]', - keyCodes.enter, - '[]' - ); - - test( - 'should insert a paragraph before the selected widget upon Shift+Enter', - '[]', - { keyCode: keyCodes.enter, shiftKey: true }, - '[]' - ); - - test( - 'should insert a paragraph when not a first-child of the root', - '[]foo', - keyCodes.enter, - '[]foo' - ); - - test( - 'should insert a paragraph when not a last-child of the root', - 'foo[]', - { keyCode: keyCodes.enter, shiftKey: true }, - 'foo[]' - ); + function test( name, data, actions, expected, expectedView, contentLanguageDirection = 'ltr' ) { + it( name, () => { + testUtils.sinon.stub( editor.locale, 'contentLanguageDirection' ).value( contentLanguageDirection ); - test( - 'should insert a paragraph only when an entire widget is selected (#1)', - '[foo] bar', - keyCodes.enter, - '[] bar' - ); + if ( !Array.isArray( actions ) ) { + actions = [ actions ]; + } - test( - 'should insert a paragraph only when an entire widget is selected (#2)', - 'f[oob]ar', - keyCodes.enter, - 'f[]ar' - ); + actions = actions.map( action => { + if ( typeof action === 'object' ) { + return action; + } - // https://github.com/ckeditor/ckeditor5/issues/1529 - it( 'should split parent when widget is inside a block element', () => { - model.schema.register( 'allowP', { - inheritAllFrom: '$block' - } ); - model.schema.register( 'disallowP', { - inheritAllFrom: '$block', - allowIn: [ 'allowP' ] + return { + keyCode: action + }; } ); - model.schema.extend( 'widget', { - allowIn: [ 'allowP', 'disallowP' ] - } ); - model.schema.extend( 'paragraph', { - allowIn: [ 'allowP' ] - } ); - - editor.conversion.for( 'downcast' ).elementToElement( { model: 'parent', view: 'parent' } ); - editor.conversion.for( 'downcast' ).elementToElement( { model: 'allowP', view: 'allowP' } ); - editor.conversion.for( 'downcast' ).elementToElement( { model: 'disallowP', view: 'disallowP' } ); - - setModelData( model, '[]' ); - - viewDocument.fire( 'keydown', new DomEventData( - viewDocument, - { target: document.createElement( 'div' ), preventDefault() {} }, - { keyCode: keyCodes.enter } - ) ); - - expect( getModelData( model ) ).to.equal( - '[]' - ); - } ); - - test( - 'should do nothing if selected is inline object', - 'foo[]bar', - keyCodes.enter, - 'foo[]bar' - ); - - test( - 'should insert a paragraph after the selected widget inside an element that is not a block upon Enter', - '
[]
', - keyCodes.enter, - '
[]
' - ); - - test( - 'should insert a paragraph before the selected widget inside an element that is not a block upon Shift+Enter', - '
[]
', - { keyCode: keyCodes.enter, shiftKey: true }, - '
[]
' - ); - } ); - - function test( name, data, keyCodeOrMock, expected, expectedView, contentLanguageDirection = 'ltr' ) { - it( name, () => { - testUtils.sinon.stub( editor.locale, 'contentLanguageDirection' ).value( contentLanguageDirection ); - - const domEventDataMock = ( typeof keyCodeOrMock == 'object' ) ? keyCodeOrMock : { - keyCode: keyCodeOrMock - }; setModelData( model, data ); - viewDocument.fire( 'keydown', new DomEventData( - viewDocument, - { target: document.createElement( 'div' ), preventDefault() {} }, - domEventDataMock - ) ); + + for ( const action of actions ) { + viewDocument.fire( 'keydown', new DomEventData( + viewDocument, + { target: document.createElement( 'div' ), preventDefault() {} }, + action + ) ); + } expect( getModelData( model ) ).to.equal( expected ); @@ -1349,18 +1337,16 @@ describe( 'Widget', () => { expect( getViewData( view ) ).to.equal( '[
' + '
' + '
' + '
' + '
' + - '
' + + '
' + '
' + '
' + '
' + @@ -1390,23 +1376,22 @@ describe( 'Widget', () => { viewDocument.fire( 'mousedown', domEventDataMock ); expect( getViewData( view ) ).to.equal( - '
' + '
' + '
' + '
' + '[
' + '
' + '
' + '
' + '
' + - '
' + '
' + '
' + @@ -1415,7 +1400,7 @@ describe( 'Widget', () => { '
' + '
]' + '
' + '
' + '
' + @@ -1443,7 +1428,6 @@ describe( 'Widget', () => { expect( getViewData( view ) ).to.equal( '[
' + '
foo bar
' + @@ -1479,12 +1463,10 @@ describe( 'Widget', () => { expect( getViewData( view ) ).to.equal( '
' + '
' + '
' + @@ -1492,10 +1474,9 @@ describe( 'Widget', () => { '
' + '[
' + - '
' + + '
' + '
' + '
' + '
' + @@ -1525,13 +1506,11 @@ describe( 'Widget', () => { expect( getViewData( view ) ).to.equal( '
' + '
[' + '
' + '
' + From 036dcd521a2ba5124e6f87ac763f5cda8cd021aa Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 4 Jun 2020 17:07:50 +0200 Subject: [PATCH 31/69] Tests: Removed obsolete CSS classes related to the WidgetTypeAround plugin. --- packages/ckeditor5-image/tests/image.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/ckeditor5-image/tests/image.js b/packages/ckeditor5-image/tests/image.js index 96003ace4f6..4fa252dee83 100644 --- a/packages/ckeditor5-image/tests/image.js +++ b/packages/ckeditor5-image/tests/image.js @@ -64,7 +64,6 @@ describe( 'Image', () => { expect( getViewData( view ) ).to.equal( '[
' + 'alt text' + @@ -82,7 +81,6 @@ describe( 'Image', () => { expect( getViewData( view ) ).to.equal( '[
' + '' + @@ -103,7 +101,6 @@ describe( 'Image', () => { expect( getViewData( view ) ).to.equal( '[
' + 'alt text' + @@ -111,7 +108,6 @@ describe( 'Image', () => { '
]' + '
' + 'alt text' + @@ -127,7 +123,6 @@ describe( 'Image', () => { expect( getViewData( view ) ).to.equal( '
' + 'alt text' + @@ -135,7 +130,6 @@ describe( 'Image', () => { '
' + '[
' + 'alt text' + From d3330244fab1ab86c0bb9992f9c8f297b95c05ef Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 4 Jun 2020 17:21:47 +0200 Subject: [PATCH 32/69] Tests: Aligned table tests to the WidgetTypeAround feature. --- .../tests/converters/upcasttable.js | 10 ++++--- .../tests/table-integration.js | 26 ++++++++++++++----- .../ckeditor5-table/tests/tablekeyboard.js | 3 +++ 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/packages/ckeditor5-table/tests/converters/upcasttable.js b/packages/ckeditor5-table/tests/converters/upcasttable.js index 5a85b4ab86b..4d160ef8f4c 100644 --- a/packages/ckeditor5-table/tests/converters/upcasttable.js +++ b/packages/ckeditor5-table/tests/converters/upcasttable.js @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting'; @@ -17,8 +17,8 @@ describe( 'upcastTable()', () => { let editor, model; beforeEach( () => { - return VirtualTestEditor - .create( { + return ClassicTestEditor + .create( '', { plugins: [ TableEditing, Paragraph, ImageEditing, Widget ] } ) .then( newEditor => { @@ -31,6 +31,10 @@ describe( 'upcastTable()', () => { } ); } ); + afterEach( () => { + editor.destroy(); + } ); + function expectModel( data ) { assertEqualMarkup( getModelData( model, { withoutSelection: true } ), data ); } diff --git a/packages/ckeditor5-table/tests/table-integration.js b/packages/ckeditor5-table/tests/table-integration.js index d00f01d0564..d74bd7fd1cb 100644 --- a/packages/ckeditor5-table/tests/table-integration.js +++ b/packages/ckeditor5-table/tests/table-integration.js @@ -11,7 +11,7 @@ import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting'; import ListEditing from '@ckeditor/ckeditor5-list/src/listediting'; import BlockQuoteEditing from '@ckeditor/ckeditor5-block-quote/src/blockquoteediting'; import Typing from '@ckeditor/ckeditor5-typing/src/typing'; -import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import { getData as getModelData, setData as setModelData @@ -27,14 +27,18 @@ describe( 'Table feature – integration', () => { let editor, clipboard; beforeEach( () => { - return VirtualTestEditor - .create( { plugins: [ Paragraph, TableEditing, ListEditing, BlockQuoteEditing, Widget, Clipboard ] } ) + return ClassicTestEditor + .create( '', { plugins: [ Paragraph, TableEditing, ListEditing, BlockQuoteEditing, Widget, Clipboard ] } ) .then( newEditor => { editor = newEditor; clipboard = editor.plugins.get( 'Clipboard' ); } ); } ); + afterEach( () => { + editor.destroy(); + } ); + it( 'pastes td as p when pasting into the table', () => { setModelData( editor.model, modelTable( [ [ 'foo[]' ] ] ) ); @@ -86,8 +90,8 @@ describe( 'Table feature – integration', () => { let editor, doc, root; beforeEach( () => { - return VirtualTestEditor - .create( { plugins: [ Paragraph, TableEditing, Widget, UndoEditing ] } ) + return ClassicTestEditor + .create( '', { plugins: [ Paragraph, TableEditing, Widget, UndoEditing ] } ) .then( newEditor => { editor = newEditor; doc = editor.model.document; @@ -95,6 +99,10 @@ describe( 'Table feature – integration', () => { } ); } ); + afterEach( () => { + editor.destroy(); + } ); + it( 'fixing empty roots should be transparent to undo', () => { expect( editor.getData( { trim: 'none' } ) ).to.equal( '

 

' ); expect( editor.commands.get( 'undo' ).isEnabled ).to.be.false; @@ -155,13 +163,17 @@ describe( 'Table feature – integration', () => { let editor; beforeEach( () => { - return VirtualTestEditor - .create( { plugins: [ Paragraph, TableEditing, ListEditing, BlockQuoteEditing, Widget, Typing ] } ) + return ClassicTestEditor + .create( '', { plugins: [ Paragraph, TableEditing, ListEditing, BlockQuoteEditing, Widget, Typing ] } ) .then( newEditor => { editor = newEditor; } ); } ); + afterEach( () => { + editor.destroy(); + } ); + it( 'merges elements without throwing errors', () => { setModelData( editor.model, modelTable( [ [ '
Foo
[]Bar' ] diff --git a/packages/ckeditor5-table/tests/tablekeyboard.js b/packages/ckeditor5-table/tests/tablekeyboard.js index fd3f35c741e..d718e6c7cf9 100644 --- a/packages/ckeditor5-table/tests/tablekeyboard.js +++ b/packages/ckeditor5-table/tests/tablekeyboard.js @@ -2769,6 +2769,9 @@ describe( 'TableKeyboard', () => { [ '20', '21', '22' ] ] ) ); + // Note: Two keydowns are necessary because the first one is handled by the WidgetTypeAround plugin + // to activate the "fake caret". + editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub ); assertEqualMarkup( getModelData( model ), modelTable( [ From b342749e9c5ea6e47ab85d38fcf7386c13edfe38 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 4 Jun 2020 17:52:38 +0200 Subject: [PATCH 33/69] Docs: Added docs to the WidgetTypeAround plugin. --- .../src/widgettypearound/widgettypearound.js | 81 ++++++++++++++++--- 1 file changed, 71 insertions(+), 10 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 52491309c49..2a0e9797901 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -76,7 +76,7 @@ export default class WidgetTypeAround extends Plugin { this._enableInsertingParagraphsOnButtonClick(); this._enableInsertingParagraphsOnEnterKeypress(); this._enableInsertingParagraphsOnUnsafeKeystroke(); - this._enableTypeAroundActivationUsingKeyboardArrows(); + this._enableTypeAroundFakeCaretActivationUsingKeyboardArrows(); } /** @@ -110,7 +110,13 @@ export default class WidgetTypeAround extends Plugin { } /** - * TODO + * Similar to {@link #_insertParagraph}, this method inserts a paragraph except that it + * does not expect a position but it performs the insertion next to a selected widget + * according to the "widget-type-around" model selection attribute value. + * + * Because this method requires the "widget-type-around" attribute to be set, + * the insertion can only happen when the widget "fake caret" is active (e.g. activated + * using the keyboard). * * @private */ @@ -188,15 +194,40 @@ export default class WidgetTypeAround extends Plugin { } /** + * Brings support for the "fake caret" that appears when either: + * + * * the selection moves from a position next to a widget (to a widget) using arrow keys, + * * the arrow key is pressed when the widget is already selected. + * + * The "fake caret" lets the user know that they can start typing or just press + * enter to insert a paragraph at the position next to a widget as suggested by the "fake caret". + * + * The "fake caret" disappears when the user changes the selection or the editor + * gets blurred. + * + * The whole idea is as follows: + * + * 1. A user does one of the 2 scenarios described at the beginning. + * 2. The "keydown" listener is executed and the decision is made whether to show or hide the "fake caret". + * 3. If it should show up, the "widget-type-around" model selection attribute is set indicating + * on which side of the widget it should appear. + * 4. The selection dispatcher reacts to the selection attribute and sets CSS classes responsible for the + * "fake caret" on the view widget. + * 5. If the "fake caret" should disappear, the selection attribute is removed and the dispatcher + * does the CSS class clean-up in the view. + * 6. Additionally, "change:range" and FocusTracker#isFocused listeners also remove the selection + * attribute (the former also removes widget CSS classes). + * * @private */ - _enableTypeAroundActivationUsingKeyboardArrows() { + _enableTypeAroundFakeCaretActivationUsingKeyboardArrows() { const editor = this.editor; const model = editor.model; const modelSelection = model.document.selection; const schema = model.schema; const editingView = editor.editing.view; + // This is the main listener responsible for the "fake caret". // Note: The priority must precede the default Widget class keydown handler. editingView.document.on( 'keydown', ( evt, domEventData ) => { if ( isArrowKeyCode( domEventData.keyCode ) ) { @@ -221,7 +252,6 @@ export default class WidgetTypeAround extends Plugin { // Get rid of the widget type around attribute of the selection on every change:range. // If the range changes, it means for sure, the user is no longer in the active ("fake horizontal caret") mode. editor.model.change( writer => { - // TODO: use data.directChange to not break collaboration? writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); } ); @@ -258,12 +288,11 @@ export default class WidgetTypeAround extends Plugin { } else { writer.removeClass( POSSIBLE_INSERTION_POSITIONS.map( positionToWidgetCssClass ), selectedViewElement ); } - }, { priority: 'highest' } ); + } ); this.listenTo( editor.ui.focusTracker, 'change:isFocused', ( evt, name, isFocused ) => { if ( !isFocused ) { editor.model.change( writer => { - // TODO: use data.directChange to not break collaboration? writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); } ); } @@ -275,7 +304,16 @@ export default class WidgetTypeAround extends Plugin { } /** - * TODO + * A listener executed on each "keydown" in the view document, a part of + * {@link #_enableTypeAroundFakeCaretActivationUsingKeyboardArrows}. + * + * It decides whether the arrow key press should activate the "fake caret" or not (also whether it should + * be deactivated). + * + * The "fake caret" activation is done by setting the "widget-type-around" model selection attribute + * in this listener and stopping&preventing the event that would normally be handled by the Widget + * plugin that is responsible for the regular keyboard navigation near/across all widgets (that + * includes inline widgets, which are ignored by the WidgetTypeAround plugin). * * @private */ @@ -370,7 +408,17 @@ export default class WidgetTypeAround extends Plugin { } /** - * TODO + * Creates the "enter" key listener on the view document that allows the user to insert a paragraph + * near the widget when either: + * + * * The "fake caret" was first activated using the arrow keys, + * * The entire widget is selected in the model. + * + * In the first case, the new paragraph is inserted according to the "widget-type-around" selection + * attribute (see {@link #_handleArrowKeyPress}). + * + * It the second case, the new paragraph is inserted based on whether a soft (Shift+Enter) keystroke + * was pressed or not. * * @private */ @@ -405,7 +453,21 @@ export default class WidgetTypeAround extends Plugin { } /** - * TODO + * Similar to the {@link #_enableInsertingParagraphsOnEnterKeypress}, it allows the user + * to insert a paragraph next to a widget when the "fake caret" was activated using arrow + * keys but it responds to "unsafe keystrokes" instead of "enter". + * + * "Unsafe keystrokes" are keystrokes that insert new content into the document + * like, for instance, letters ("a") or numbers ("4"). The "keydown" listener enabled by this method + * will insert a new paragraph according to the "widget-type-around" model selection attribute + * as the user simply starts typing, which creates the impression that the "fake caret" + * behaves like a "real one" rendered by the browser (AKA your text appears where the caret was). + * + * **Note**: ATM this listener creates 2 undo steps: one for the "insertParagraph" command + * and the second for the actual typing. It's not a disaster but this may need to be fixed + * sooner or later. + * + * Learn more in {@link module:typing/utils/injectunsafekeystrokeshandling}. * * @private */ @@ -417,7 +479,6 @@ export default class WidgetTypeAround extends Plugin { editingView.document.on( 'keydown', ( evt, domEventData ) => { // Don't handle enter here. It's handled in a separate listener. if ( domEventData.keyCode !== keyCodes.enter && !isSafeKeystroke( domEventData ) ) { - // TODO: Extra undo step problem. this._insertParagraphAccordingToSelectionAttribute(); } }, { priority: priorities.get( 'high' ) + 1 } ); From dddc9ce79dcd6e26324d3941d382d942cef224ac Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Fri, 5 Jun 2020 11:17:32 +0200 Subject: [PATCH 34/69] Hide the widget selection handle when the "fake caret" is either before or after the widget. --- .../ckeditor5-widget/widgettypearound.css | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css index e72c456be09..0ceeea8b742 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css @@ -151,23 +151,23 @@ } } - /* - * Styles of the type around buttons when the "fake caret" is blinking (e.g. upon keyboard navigation). - * In this state, the type around buttons would collide with the fake carets so they should disappear. - */ &.ck-widget_type-around_show-fake-caret_before, &.ck-widget_type-around_show-fake-caret_after { + /* + * Styles of the type around buttons when the "fake caret" is blinking (e.g. upon keyboard navigation). + * In this state, the type around buttons would collide with the fake carets so they should disappear. + */ & > .ck-widget__type-around > .ck-widget__type-around__button { @mixin ck-widget-type-around-button-hidden; } - } - /* - * Fake horizontal caret integration with the selection handle. When the caret is visible, simply - * hide the handle because it intersects with the caret (and does not make much sense anyway). - */ - &.ck-widget_type-around_show-fake-caret_before.ck-widget_with-selection-handle > .ck-widget__selection-handle { - display: none; + /* + * Fake horizontal caret integration with the selection handle. When the caret is visible, simply + * hide the handle because it intersects with the caret (and does not make much sense anyway). + */ + &.ck-widget_with-selection-handle > .ck-widget__selection-handle { + display: none; + } } } From 6be9c2bae062fb99ce779008d62968e3136bb6b9 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Fri, 5 Jun 2020 11:23:58 +0200 Subject: [PATCH 35/69] Code refactoring. --- .../src/widgettypearound/widgettypearound.js | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 2a0e9797901..22b862dfb59 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -165,34 +165,6 @@ export default class WidgetTypeAround extends Plugin { }, { priority: 'low' } ); } - /** - * Registers a `mousedown` listener for the view document which intercepts events - * coming from the type around UI, which happens when a user clicks one of the buttons - * that insert a paragraph next to a widget. - * - * @private - */ - _enableInsertingParagraphsOnButtonClick() { - const editor = this.editor; - const editingView = editor.editing.view; - - editingView.document.on( 'mousedown', ( evt, domEventData ) => { - const button = getClosestTypeAroundDomButton( domEventData.domTarget ); - - if ( !button ) { - return; - } - - const buttonPosition = getTypeAroundButtonPosition( button ); - const widgetViewElement = getClosestWidgetViewElement( button, editingView.domConverter ); - - this._insertParagraph( widgetViewElement, buttonPosition ); - - domEventData.preventDefault(); - evt.stop(); - } ); - } - /** * Brings support for the "fake caret" that appears when either: * @@ -407,6 +379,34 @@ export default class WidgetTypeAround extends Plugin { } } + /** + * Registers a `mousedown` listener for the view document which intercepts events + * coming from the type around UI, which happens when a user clicks one of the buttons + * that insert a paragraph next to a widget. + * + * @private + */ + _enableInsertingParagraphsOnButtonClick() { + const editor = this.editor; + const editingView = editor.editing.view; + + editingView.document.on( 'mousedown', ( evt, domEventData ) => { + const button = getClosestTypeAroundDomButton( domEventData.domTarget ); + + if ( !button ) { + return; + } + + const buttonPosition = getTypeAroundButtonPosition( button ); + const widgetViewElement = getClosestWidgetViewElement( button, editingView.domConverter ); + + this._insertParagraph( widgetViewElement, buttonPosition ); + + domEventData.preventDefault(); + evt.stop(); + } ); + } + /** * Creates the "enter" key listener on the view document that allows the user to insert a paragraph * near the widget when either: From bc9a93e2d02692e60d6e9731d609c61d9b6f18a5 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Fri, 5 Jun 2020 15:15:01 +0200 Subject: [PATCH 36/69] Implemented a WidgetTypeAround integration with the delete/backspace. --- .../src/widgettypearound/widgettypearound.js | 77 ++++++- .../widgettypearound/widgettypearound.js | 188 ++++++++++++++++++ 2 files changed, 263 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 22b862dfb59..ae7497dcafe 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -77,6 +77,7 @@ export default class WidgetTypeAround extends Plugin { this._enableInsertingParagraphsOnEnterKeypress(); this._enableInsertingParagraphsOnUnsafeKeystroke(); this._enableTypeAroundFakeCaretActivationUsingKeyboardArrows(); + this._enableDeleteIntegration(); } /** @@ -474,15 +475,87 @@ export default class WidgetTypeAround extends Plugin { _enableInsertingParagraphsOnUnsafeKeystroke() { const editor = this.editor; const editingView = editor.editing.view; + const keyCodesHandledSomewhereElse = [ + keyCodes.enter, + keyCodes.delete, + keyCodes.backspace + ]; // Note: The priority must precede the default Widget class keydown handler. editingView.document.on( 'keydown', ( evt, domEventData ) => { - // Don't handle enter here. It's handled in a separate listener. - if ( domEventData.keyCode !== keyCodes.enter && !isSafeKeystroke( domEventData ) ) { + // Don't handle enter/backspace/delete here. They are handled in dedicated listeners. + if ( !keyCodesHandledSomewhereElse.includes( domEventData.keyCode ) && !isSafeKeystroke( domEventData ) ) { this._insertParagraphAccordingToSelectionAttribute(); } }, { priority: priorities.get( 'high' ) + 1 } ); } + + /** + * It creates a "delete" event listener on the view document to handle cases when delete/backspace + * is pressed and the "fake caret" is currently active. + * + * The "fake caret" should create an illusion of a "real browser caret" so that when it appears + * before/after a widget, pressing delete/backspace should remove a widget or delete a content + * before/after a widget (depending on the content surrounding the widget). + * + * @private + */ + _enableDeleteIntegration() { + const editor = this.editor; + const editingView = editor.editing.view; + const model = editor.model; + const schema = model.schema; + + // Note: The priority must precede the default Widget class delete handler. + this.listenTo( editingView.document, 'delete', ( evt, domEventData ) => { + const typeAroundSelectionAttributeValue = model.document.selection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); + + // This listener handles only these cases when the "fake caret" is active. + if ( !typeAroundSelectionAttributeValue ) { + return false; + } + + const direction = domEventData.direction; + const selectedModelWidget = model.document.selection.getSelectedElement(); + + if ( typeAroundSelectionAttributeValue === 'before' ) { + if ( direction === 'backward' ) { + const range = schema.getNearestSelectionRange( model.createPositionBefore( selectedModelWidget ), direction ); + + if ( range ) { + model.change( writer => { + writer.setSelection( range ); + editor.execute( 'delete' ); + } ); + } + } else { + editor.execute( 'delete', { + selection: model.createSelection( selectedModelWidget, 'on' ) + } ); + } + } else { + if ( direction === 'backward' ) { + editor.execute( 'delete', { + selection: model.createSelection( selectedModelWidget, 'on' ) + } ); + } else { + const range = schema.getNearestSelectionRange( model.createPositionAfter( selectedModelWidget ), direction ); + + if ( range ) { + model.change( writer => { + writer.setSelection( range ); + editor.execute( 'forwardDelete' ); + } ); + } + } + } + + // If some content was deleted, don't let the handler from the Widget plugin kick in. + // If nothing was deleted, then the default handler will have nothing to do anyway. + domEventData.preventDefault(); + evt.stop(); + }, { priority: priorities.get( 'high' ) + 1 } ); + } } // Injects the type around UI into a view widget instance. diff --git a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js index ce574bec125..3cc7b0f671a 100644 --- a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js @@ -875,6 +875,194 @@ describe( 'WidgetTypeAround', () => { } ); } ); + describe( 'delete integration', () => { + let eventInfoStub, domEventDataStub; + + describe( 'backward delete', () => { + it( 'should delete content before a widget if the "fake caret" is also before the widget', () => { + setModelData( editor.model, 'foo[]' ); + + fireKeyboardEvent( 'arrowleft' ); + + expect( getModelData( model ) ).to.equal( 'foo[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + fireDeleteEvent(); + expect( getModelData( model ) ).to.equal( 'fo[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should do nothing if the "fake caret" is before the widget but there is nothing to delete there', () => { + setModelData( editor.model, '[]' ); + + fireKeyboardEvent( 'arrowleft' ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + fireDeleteEvent(); + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should delete a widget if the "fake caret" is after the widget (no content after the widget)', () => { + setModelData( editor.model, '[]' ); + + fireKeyboardEvent( 'arrowright' ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + + fireDeleteEvent(); + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should delete a widget if the "fake caret" is after the widget (some content after the widget)', () => { + setModelData( editor.model, '[]foo' ); + + fireKeyboardEvent( 'arrowright' ); + + expect( getModelData( model ) ).to.equal( '[]foo' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + + fireDeleteEvent(); + expect( getModelData( model ) ).to.equal( '[]foo' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should delete a sibling widget', () => { + setModelData( editor.model, 'foo[]' ); + + fireKeyboardEvent( 'arrowleft' ); + + expect( getModelData( model ) ).to.equal( + 'foo' + + '[]' + ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + fireDeleteEvent(); + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + } ); + + describe( 'forward delete', () => { + it( 'should delete content after a widget if the "fake caret" is also after the widget', () => { + setModelData( editor.model, '[]foo' ); + + fireKeyboardEvent( 'arrowright' ); + + expect( getModelData( model ) ).to.equal( '[]foo' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + + fireDeleteEvent( true ); + expect( getModelData( model ) ).to.equal( '[]oo' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should do nothing if the "fake caret" is after the widget but there is nothing to delete there', () => { + setModelData( editor.model, '[]' ); + + fireKeyboardEvent( 'arrowright' ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + + fireDeleteEvent( true ); + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should delete a widget if the "fake caret" is before the widget (no content before the widget)', () => { + setModelData( editor.model, '[]' ); + + fireKeyboardEvent( 'arrowleft' ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + fireDeleteEvent( true ); + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should delete a widget if the "fake caret" is before the widget (some content before the widget)', () => { + setModelData( editor.model, 'foo[]' ); + + fireKeyboardEvent( 'arrowleft' ); + + expect( getModelData( model ) ).to.equal( 'foo[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + fireDeleteEvent( true ); + expect( getModelData( model ) ).to.equal( 'foo[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should delete a sibling widget', () => { + setModelData( editor.model, '[]foo' ); + + fireKeyboardEvent( 'arrowright' ); + + expect( getModelData( model ) ).to.equal( + '[]' + + 'foo' + ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + + fireDeleteEvent( true ); + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + } ); + + function fireDeleteEvent( isForward = false ) { + eventInfoStub = new EventInfo( viewDocument, 'delete' ); + sinon.spy( eventInfoStub, 'stop' ); + + const data = { + direction: isForward ? 'forward' : 'backward', + unit: 'character' + }; + + domEventDataStub = new DomEventData( viewDocument, getDomEvent(), data ); + + viewDocument.fire( eventInfoStub, domEventDataStub ); + } + } ); + function getDomEvent() { return { preventDefault: sinon.spy(), From a6da635d3f9b3d5ce0557307335af769d07be555 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 9 Jun 2020 15:38:46 +0200 Subject: [PATCH 37/69] Update packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js Co-authored-by: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> --- .../tests/utils/injectunsafekeystrokeshandling.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js b/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js index d7cfc1e5734..1d65385f8ed 100644 --- a/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js +++ b/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js @@ -12,7 +12,7 @@ describe( 'unsafe keystroke handling utils', () => { describe( 'isSafeKeystroke()', () => { it( 'should return "true" for any keystroke with the Ctrl key', () => { expect( isSafeKeystroke( { keyCode: keyCodes.a, ctrlKey: true } ), 'Ctrl+a' ).to.be.true; - expect( isSafeKeystroke( { keyCode: keyCodes[ 48 ], ctrlKey: true } ), 'Ctrk+0' ).to.be.true; + expect( isSafeKeystroke( { keyCode: keyCodes[ 0 ], ctrlKey: true } ), 'Ctrl+0' ).to.be.true; } ); it( 'should return "true" for all arrow keys', () => { From f80bef271503ba44e9aa710fe5a237d810069353 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 9 Jun 2020 15:41:41 +0200 Subject: [PATCH 38/69] Update packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js Co-authored-by: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> --- .../tests/utils/injectunsafekeystrokeshandling.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js b/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js index 1d65385f8ed..e12e6ed4519 100644 --- a/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js +++ b/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js @@ -78,7 +78,7 @@ describe( 'unsafe keystroke handling utils', () => { it( 'should return "false" for the keystrokes that result in typing', () => { expect( isSafeKeystroke( { keyCode: keyCodes.a } ), 'a' ).to.be.false; - expect( isSafeKeystroke( { keyCode: keyCodes[ 48 ] } ), '0' ).to.be.false; + expect( isSafeKeystroke( { keyCode: keyCodes[ 0 ] } ), '0' ).to.be.false; expect( isSafeKeystroke( { keyCode: keyCodes.a, altKey: true } ), 'Alt+a' ).to.be.false; } ); } ); From 4691d856f2b9942dadfbab1763cd4f692fab7758 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 9 Jun 2020 15:11:06 +0200 Subject: [PATCH 39/69] Restored the widget :hover outline when the type around "fake caret" is visible. --- .../theme/ckeditor5-widget/widgettypearound.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css index 0ceeea8b742..c01f3d787b3 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css @@ -143,8 +143,7 @@ * Styles of the widget when the "fake caret" is blinking (e.g. upon keyboard navigation). * Despite the widget being physically selected in the model, its outline should disappear. */ - &.ck-widget_selected, - &.ck-widget.ck-widget_selected:hover { + &.ck-widget_selected { &.ck-widget_type-around_show-fake-caret_before, &.ck-widget_type-around_show-fake-caret_after { outline-color: transparent; From cf9cca7a59b01ff81f3fa75c214d2d6f46346d5c Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 9 Jun 2020 15:14:39 +0200 Subject: [PATCH 40/69] The widget selection handle should slowly fade out when the type around fake caret shows up. --- .../theme/ckeditor5-widget/widgettypearound.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css index c01f3d787b3..804c50356f8 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css @@ -164,8 +164,8 @@ * Fake horizontal caret integration with the selection handle. When the caret is visible, simply * hide the handle because it intersects with the caret (and does not make much sense anyway). */ - &.ck-widget_with-selection-handle > .ck-widget__selection-handle { - display: none; + &.ck-widget_selected.ck-widget_with-selection-handle > .ck-widget__selection-handle { + opacity: 0 } } } From 18f68f0b505124fd43f36b09d8cb018f3079ec8d Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 9 Jun 2020 15:45:18 +0200 Subject: [PATCH 41/69] Renamed isSafeKeystroke() to isNonTypingKeystroke(). Added docs for the helper. --- .../utils/injectunsafekeystrokeshandling.js | 11 +- .../utils/injectunsafekeystrokeshandling.js | 118 +++++++++--------- .../src/widgettypearound/widgettypearound.js | 4 +- 3 files changed, 69 insertions(+), 64 deletions(-) diff --git a/packages/ckeditor5-typing/src/utils/injectunsafekeystrokeshandling.js b/packages/ckeditor5-typing/src/utils/injectunsafekeystrokeshandling.js index 31058042ede..718fac76cdc 100644 --- a/packages/ckeditor5-typing/src/utils/injectunsafekeystrokeshandling.js +++ b/packages/ckeditor5-typing/src/utils/injectunsafekeystrokeshandling.js @@ -65,7 +65,7 @@ export default function injectUnsafeKeystrokesHandling( editor ) { return; } - if ( isSafeKeystroke( evtData ) || doc.selection.isCollapsed ) { + if ( isNonTypingKeystroke( evtData ) || doc.selection.isCollapsed ) { return; } @@ -155,14 +155,19 @@ for ( let code = 112; code <= 135; code++ ) { } /** - * Returns `true` if a keystroke will not result in "typing". + * Returns `true` if a keystroke will **not** result in "typing". + * + * For instance, keystrokes that result in typing are letters "a-zA-Z", numbers "0-9", delete, backspace, etc. + * + * Keystrokes that do not cause typing are, for instance, Fn keys (F5, F8, etc.), arrow keys (←, →, ↑, ↓), + * Tab (↹), "Windows logo key" (⊞ Win), etc. * * Note: This implementation is very simple and will need to be refined with time. * * @param {module:engine/view/observer/keyobserver~KeyEventData} keyData * @returns {Boolean} */ -export function isSafeKeystroke( keyData ) { +export function isNonTypingKeystroke( keyData ) { // Keystrokes which contain Ctrl don't represent typing. if ( keyData.ctrlKey ) { return true; diff --git a/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js b/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js index e12e6ed4519..63090ddb687 100644 --- a/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js +++ b/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js @@ -6,80 +6,80 @@ import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; -import { isSafeKeystroke } from '../../src/utils/injectunsafekeystrokeshandling'; +import { isNonTypingKeystroke } from '../../src/utils/injectunsafekeystrokeshandling'; describe( 'unsafe keystroke handling utils', () => { - describe( 'isSafeKeystroke()', () => { + describe( 'isNonTypingKeystroke()', () => { it( 'should return "true" for any keystroke with the Ctrl key', () => { - expect( isSafeKeystroke( { keyCode: keyCodes.a, ctrlKey: true } ), 'Ctrl+a' ).to.be.true; - expect( isSafeKeystroke( { keyCode: keyCodes[ 0 ], ctrlKey: true } ), 'Ctrl+0' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes.a, ctrlKey: true } ), 'Ctrl+a' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes[ 0 ], ctrlKey: true } ), 'Ctrk+0' ).to.be.true; } ); it( 'should return "true" for all arrow keys', () => { - expect( isSafeKeystroke( { keyCode: keyCodes.arrowup } ), 'arrow up' ).to.be.true; - expect( isSafeKeystroke( { keyCode: keyCodes.arrowdown } ), 'arrow down' ).to.be.true; - expect( isSafeKeystroke( { keyCode: keyCodes.arrowleft } ), 'arrow left' ).to.be.true; - expect( isSafeKeystroke( { keyCode: keyCodes.arrowright } ), 'arrow right' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes.arrowup } ), 'arrow up' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes.arrowdown } ), 'arrow down' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes.arrowleft } ), 'arrow left' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes.arrowright } ), 'arrow right' ).to.be.true; } ); it( 'should return "true" for function (Fn) keystrokes', () => { - expect( isSafeKeystroke( { keyCode: 112 } ), 'F1' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 113 } ), 'F2' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 114 } ), 'F3' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 115 } ), 'F4' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 116 } ), 'F5' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 117 } ), 'F6' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 118 } ), 'F7' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 119 } ), 'F8' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 120 } ), 'F9' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 121 } ), 'F10' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 122 } ), 'F11' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 123 } ), 'F12' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 124 } ), 'F13' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 125 } ), 'F14' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 126 } ), 'F15' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 127 } ), 'F16' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 128 } ), 'F17' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 129 } ), 'F18' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 130 } ), 'F19' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 131 } ), 'F20' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 132 } ), 'F21' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 133 } ), 'F22' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 134 } ), 'F23' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 135 } ), 'F24' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 112 } ), 'F1' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 113 } ), 'F2' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 114 } ), 'F3' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 115 } ), 'F4' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 116 } ), 'F5' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 117 } ), 'F6' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 118 } ), 'F7' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 119 } ), 'F8' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 120 } ), 'F9' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 121 } ), 'F10' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 122 } ), 'F11' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 123 } ), 'F12' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 124 } ), 'F13' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 125 } ), 'F14' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 126 } ), 'F15' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 127 } ), 'F16' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 128 } ), 'F17' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 129 } ), 'F18' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 130 } ), 'F19' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 131 } ), 'F20' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 132 } ), 'F21' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 133 } ), 'F22' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 134 } ), 'F23' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 135 } ), 'F24' ).to.be.true; } ); it( 'should return "true" for other safe keystrokes', () => { - expect( isSafeKeystroke( { keyCode: 9 } ), 'Tab' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 16 } ), 'Shift' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 17 } ), 'Ctrl' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 18 } ), 'Alt' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 19 } ), 'Pause' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 20 } ), 'CapsLock' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 27 } ), 'Escape' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 33 } ), 'PageUp' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 34 } ), 'PageDown' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 35 } ), 'Home' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 36 } ), 'End,' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 45 } ), 'Insert' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 91 } ), 'Windows' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 93 } ), 'Menu key' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 144 } ), 'NumLock' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 145 } ), 'ScrollLock' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 173 } ), 'Mute/Unmute' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 174 } ), 'Volume up' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 175 } ), 'Volume down' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 176 } ), 'Next song' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 177 } ), 'Previous song' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 178 } ), 'Stop' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 179 } ), 'Play/Pause' ).to.be.true; - expect( isSafeKeystroke( { keyCode: 255 } ), 'Display brightness (increase and decrease)' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 9 } ), 'Tab' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 16 } ), 'Shift' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 17 } ), 'Ctrl' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 18 } ), 'Alt' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 19 } ), 'Pause' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 20 } ), 'CapsLock' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 27 } ), 'Escape' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 33 } ), 'PageUp' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 34 } ), 'PageDown' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 35 } ), 'Home' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 36 } ), 'End,' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 45 } ), 'Insert' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 91 } ), 'Windows' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 93 } ), 'Menu key' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 144 } ), 'NumLock' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 145 } ), 'ScrollLock' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 173 } ), 'Mute/Unmute' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 174 } ), 'Volume up' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 175 } ), 'Volume down' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 176 } ), 'Next song' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 177 } ), 'Previous song' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 178 } ), 'Stop' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 179 } ), 'Play/Pause' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 255 } ), 'Display brightness (increase and decrease)' ).to.be.true; } ); it( 'should return "false" for the keystrokes that result in typing', () => { - expect( isSafeKeystroke( { keyCode: keyCodes.a } ), 'a' ).to.be.false; - expect( isSafeKeystroke( { keyCode: keyCodes[ 0 ] } ), '0' ).to.be.false; - expect( isSafeKeystroke( { keyCode: keyCodes.a, altKey: true } ), 'Alt+a' ).to.be.false; + expect( isNonTypingKeystroke( { keyCode: keyCodes.a } ), 'a' ).to.be.false; + expect( isNonTypingKeystroke( { keyCode: keyCodes[ 0 ] } ), '0' ).to.be.false; + expect( isNonTypingKeystroke( { keyCode: keyCodes.a, altKey: true } ), 'Alt+a' ).to.be.false; } ); } ); } ); diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index ae7497dcafe..45ae788d14a 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -27,7 +27,7 @@ import { } from './utils'; import { - isSafeKeystroke + isNonTypingKeystroke } from '@ckeditor/ckeditor5-typing/src/utils/injectunsafekeystrokeshandling'; import returnIcon from '../../theme/icons/return-arrow.svg'; @@ -484,7 +484,7 @@ export default class WidgetTypeAround extends Plugin { // Note: The priority must precede the default Widget class keydown handler. editingView.document.on( 'keydown', ( evt, domEventData ) => { // Don't handle enter/backspace/delete here. They are handled in dedicated listeners. - if ( !keyCodesHandledSomewhereElse.includes( domEventData.keyCode ) && !isSafeKeystroke( domEventData ) ) { + if ( !keyCodesHandledSomewhereElse.includes( domEventData.keyCode ) && !isNonTypingKeystroke( domEventData ) ) { this._insertParagraphAccordingToSelectionAttribute(); } }, { priority: priorities.get( 'high' ) + 1 } ); From 5e527b9f6a5718315c4eb72bc7fc428cc7713b31 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 9 Jun 2020 15:43:48 +0200 Subject: [PATCH 42/69] Tests: Code refactoring in isNonTypingKeystroke() tests. --- .../utils/injectunsafekeystrokeshandling.js | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js b/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js index 63090ddb687..74dad1f535f 100644 --- a/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js +++ b/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js @@ -23,18 +23,18 @@ describe( 'unsafe keystroke handling utils', () => { } ); it( 'should return "true" for function (Fn) keystrokes', () => { - expect( isNonTypingKeystroke( { keyCode: 112 } ), 'F1' ).to.be.true; - expect( isNonTypingKeystroke( { keyCode: 113 } ), 'F2' ).to.be.true; - expect( isNonTypingKeystroke( { keyCode: 114 } ), 'F3' ).to.be.true; - expect( isNonTypingKeystroke( { keyCode: 115 } ), 'F4' ).to.be.true; - expect( isNonTypingKeystroke( { keyCode: 116 } ), 'F5' ).to.be.true; - expect( isNonTypingKeystroke( { keyCode: 117 } ), 'F6' ).to.be.true; - expect( isNonTypingKeystroke( { keyCode: 118 } ), 'F7' ).to.be.true; - expect( isNonTypingKeystroke( { keyCode: 119 } ), 'F8' ).to.be.true; - expect( isNonTypingKeystroke( { keyCode: 120 } ), 'F9' ).to.be.true; - expect( isNonTypingKeystroke( { keyCode: 121 } ), 'F10' ).to.be.true; - expect( isNonTypingKeystroke( { keyCode: 122 } ), 'F11' ).to.be.true; - expect( isNonTypingKeystroke( { keyCode: 123 } ), 'F12' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes.f1 } ), 'F1' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes.f2 } ), 'F2' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes.f3 } ), 'F3' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes.f4 } ), 'F4' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes.f5 } ), 'F5' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes.f6 } ), 'F6' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes.f7 } ), 'F7' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes.f8 } ), 'F8' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes.f9 } ), 'F9' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes.f10 } ), 'F10' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes.f11 } ), 'F11' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes.f12 } ), 'F12' ).to.be.true; expect( isNonTypingKeystroke( { keyCode: 124 } ), 'F13' ).to.be.true; expect( isNonTypingKeystroke( { keyCode: 125 } ), 'F14' ).to.be.true; expect( isNonTypingKeystroke( { keyCode: 126 } ), 'F15' ).to.be.true; @@ -50,17 +50,17 @@ describe( 'unsafe keystroke handling utils', () => { } ); it( 'should return "true" for other safe keystrokes', () => { - expect( isNonTypingKeystroke( { keyCode: 9 } ), 'Tab' ).to.be.true; - expect( isNonTypingKeystroke( { keyCode: 16 } ), 'Shift' ).to.be.true; - expect( isNonTypingKeystroke( { keyCode: 17 } ), 'Ctrl' ).to.be.true; - expect( isNonTypingKeystroke( { keyCode: 18 } ), 'Alt' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes.tab } ), 'Tab' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes.shift } ), 'Shift' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes.ctrl } ), 'Ctrl' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes.alt } ), 'Alt' ).to.be.true; expect( isNonTypingKeystroke( { keyCode: 19 } ), 'Pause' ).to.be.true; expect( isNonTypingKeystroke( { keyCode: 20 } ), 'CapsLock' ).to.be.true; - expect( isNonTypingKeystroke( { keyCode: 27 } ), 'Escape' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes.esc } ), 'Escape' ).to.be.true; expect( isNonTypingKeystroke( { keyCode: 33 } ), 'PageUp' ).to.be.true; expect( isNonTypingKeystroke( { keyCode: 34 } ), 'PageDown' ).to.be.true; expect( isNonTypingKeystroke( { keyCode: 35 } ), 'Home' ).to.be.true; - expect( isNonTypingKeystroke( { keyCode: 36 } ), 'End,' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 36 } ), 'End' ).to.be.true; expect( isNonTypingKeystroke( { keyCode: 45 } ), 'Insert' ).to.be.true; expect( isNonTypingKeystroke( { keyCode: 91 } ), 'Windows' ).to.be.true; expect( isNonTypingKeystroke( { keyCode: 93 } ), 'Menu key' ).to.be.true; From b0e273efb433a3012443652550009be7b76e83b8 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 9 Jun 2020 15:46:32 +0200 Subject: [PATCH 43/69] Tests: Code refactoring in isNonTypingKeystroke() tests. --- .../tests/utils/injectunsafekeystrokeshandling.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js b/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js index 74dad1f535f..f483eda5e78 100644 --- a/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js +++ b/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js @@ -12,7 +12,7 @@ describe( 'unsafe keystroke handling utils', () => { describe( 'isNonTypingKeystroke()', () => { it( 'should return "true" for any keystroke with the Ctrl key', () => { expect( isNonTypingKeystroke( { keyCode: keyCodes.a, ctrlKey: true } ), 'Ctrl+a' ).to.be.true; - expect( isNonTypingKeystroke( { keyCode: keyCodes[ 0 ], ctrlKey: true } ), 'Ctrk+0' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: keyCodes[ 0 ], ctrlKey: true } ), 'Ctrl+0' ).to.be.true; } ); it( 'should return "true" for all arrow keys', () => { From 0cb585f57e0e9cee7c878727ef7ccd1f680b533c Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 9 Jun 2020 15:50:49 +0200 Subject: [PATCH 44/69] Docs: Extended docs of various keyboard helpers. --- packages/ckeditor5-utils/src/keyboard.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-utils/src/keyboard.js b/packages/ckeditor5-utils/src/keyboard.js index 8bcbb82aa40..b7a64f138f1 100644 --- a/packages/ckeditor5-utils/src/keyboard.js +++ b/packages/ckeditor5-utils/src/keyboard.js @@ -132,7 +132,7 @@ export function getEnvKeystrokeText( keystroke ) { /** * Returns `true` if the provided key code represents one of the arrow keys. * - * @param {Number} keyCode + * @param {Number} keyCode A key code as in {@link module:utils/keyboard~KeystrokeInfo#keyCode}. * @returns {Boolean} */ export function isArrowKeyCode( keyCode ) { @@ -149,7 +149,7 @@ export function isArrowKeyCode( keyCode ) { * For instance, in right–to–left (RTL) content languages, pressing the left arrow means moving selection right (forward) * in the model structure. Similarly, pressing the right arrow moves the selection left (backward). * - * @param {Number} keyCode + * @param {Number} keyCode A key code as in {@link module:utils/keyboard~KeystrokeInfo#keyCode}. * @param {'ltr'|'rtl'} contentLanguageDirection The content language direction, corresponding to * {@link module:utils/locale~Locale#contentLanguageDirection}. * @returns {'left'|'up'|'right'|'down'} Localized arrow direction. @@ -179,7 +179,7 @@ export function getLocalizedArrowKeyCodeDirection( keyCode, contentLanguageDirec * For instance, in right–to–left (RTL) languages, pressing the left arrow means moving forward * in the model structure. Similarly, pressing the right arrow moves the selection backward. * - * @param {Number} keyCode + * @param {Number} keyCode A key code as in {@link module:utils/keyboard~KeystrokeInfo#keyCode}. * @param {'ltr'|'rtl'} contentLanguageDirection The content language direction, corresponding to * {@link module:utils/locale~Locale#contentLanguageDirection}. * @returns {Boolean} From c77db0a00fd0779fa8c8ef5110285568abb91baf Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 9 Jun 2020 15:51:54 +0200 Subject: [PATCH 45/69] Added a missing dependency in package.json. --- packages/ckeditor5-widget/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ckeditor5-widget/package.json b/packages/ckeditor5-widget/package.json index 9e36d380cb1..64fa1d01784 100644 --- a/packages/ckeditor5-widget/package.json +++ b/packages/ckeditor5-widget/package.json @@ -11,6 +11,7 @@ "dependencies": { "@ckeditor/ckeditor5-core": "^19.0.1", "@ckeditor/ckeditor5-engine": "^19.0.1", + "@ckeditor/ckeditor5-typing": "^19.0.1", "@ckeditor/ckeditor5-ui": "^19.0.1", "@ckeditor/ckeditor5-utils": "^19.0.1", "lodash-es": "^4.17.15" From 415f0bf80906e37ca877ac4a92a8938217c4cb60 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 9 Jun 2020 15:57:21 +0200 Subject: [PATCH 46/69] Code refactoring: Got rid of the Paragraph dependency in the WidgetTypeAround plugin. --- .../src/widgettypearound/widgettypearound.js | 8 -------- packages/ckeditor5-widget/tests/widget-integration.js | 3 ++- packages/ckeditor5-widget/tests/widget.js | 5 +++-- .../tests/widgettypearound/widgettypearound.js | 7 +++---- 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 45ae788d14a..e0844b824b3 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -10,7 +10,6 @@ */ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Template from '@ckeditor/ckeditor5-ui/src/template'; import { isArrowKeyCode, @@ -54,13 +53,6 @@ const TYPE_AROUND_SELECTION_ATTRIBUTE = 'widget-type-around'; * @private */ export default class WidgetTypeAround extends Plugin { - /** - * @inheritDoc - */ - static get requires() { - return [ Paragraph ]; - } - /** * @inheritDoc */ diff --git a/packages/ckeditor5-widget/tests/widget-integration.js b/packages/ckeditor5-widget/tests/widget-integration.js index 7e73f093897..12d8f0e2cbd 100644 --- a/packages/ckeditor5-widget/tests/widget-integration.js +++ b/packages/ckeditor5-widget/tests/widget-integration.js @@ -6,6 +6,7 @@ /* global document */ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Typing from '@ckeditor/ckeditor5-typing/src/typing'; import Widget from '../src/widget'; import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata'; @@ -31,7 +32,7 @@ describe( 'Widget - integration', () => { editorElement = document.createElement( 'div' ); document.body.appendChild( editorElement ); - return ClassicEditor.create( editorElement, { plugins: [ Widget, Typing ] } ) + return ClassicEditor.create( editorElement, { plugins: [ Paragraph, Widget, Typing ] } ) .then( newEditor => { editor = newEditor; model = editor.model; diff --git a/packages/ckeditor5-widget/tests/widget.js b/packages/ckeditor5-widget/tests/widget.js index c07b69d1aa5..d6ea7e13f29 100644 --- a/packages/ckeditor5-widget/tests/widget.js +++ b/packages/ckeditor5-widget/tests/widget.js @@ -7,6 +7,7 @@ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import Enter from '@ckeditor/ckeditor5-enter/src/enter'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Widget from '../src/widget'; import WidgetTypeAround from '../src/widgettypearound/widgettypearound'; import Typing from '@ckeditor/ckeditor5-typing/src/typing'; @@ -29,7 +30,7 @@ describe( 'Widget', () => { return ClassicTestEditor .create( element, { - plugins: [ Widget, Typing, Enter ] + plugins: [ Paragraph, Widget, Typing, Enter ] } ) .then( newEditor => { editor = newEditor; @@ -1262,7 +1263,7 @@ describe( 'Widget', () => { return ClassicTestEditor .create( element, { - plugins: [ Widget, Typing ] + plugins: [ Paragraph, Widget, Typing ] } ) .then( newEditor => { editor = newEditor; diff --git a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js index 3cc7b0f671a..2a5b143572b 100644 --- a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js @@ -5,7 +5,6 @@ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; -import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata'; import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; @@ -46,12 +45,12 @@ describe( 'WidgetTypeAround', () => { } ); describe( 'plugin', () => { - it( 'is loaded', () => { + it( 'should be loaded', () => { expect( editor.plugins.get( WidgetTypeAround ) ).to.be.instanceOf( WidgetTypeAround ); } ); - it( 'requires the Paragraph plugin', () => { - expect( WidgetTypeAround.requires ).to.deep.equal( [ Paragraph ] ); + it( 'should have a name', () => { + expect( WidgetTypeAround.pluginName ).to.equal( 'WidgetTypeAround' ); } ); } ); From abda6ea08c8f37af3115e5596f54de3263cebc9a Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 9 Jun 2020 15:59:55 +0200 Subject: [PATCH 47/69] Docs: Added missing docs in the WidgetTypeAround#_insertParagraphAccordingToSelectionAttribute method. --- .../ckeditor5-widget/src/widgettypearound/widgettypearound.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index e0844b824b3..57912e3e4fb 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -112,6 +112,7 @@ export default class WidgetTypeAround extends Plugin { * using the keyboard). * * @private + * @returns {Boolean} Returns `true` when the paragraph was inserted (the attribute was present) and `false` otherwise. */ _insertParagraphAccordingToSelectionAttribute() { const editor = this.editor; From 0888cc3aa238da771de3fc53c62bc7d861966884 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 9 Jun 2020 16:05:58 +0200 Subject: [PATCH 48/69] Code refactoring: Increased the priority of the keydown listeners in the WidgetTypeAround to avoid future collisions with the TableKeyboard plugin. --- .../src/widgettypearound/widgettypearound.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 57912e3e4fb..bc646ca4d97 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -67,7 +67,7 @@ export default class WidgetTypeAround extends Plugin { this._enableTypeAroundUIInjection(); this._enableInsertingParagraphsOnButtonClick(); this._enableInsertingParagraphsOnEnterKeypress(); - this._enableInsertingParagraphsOnUnsafeKeystroke(); + this._enableInsertingParagraphsOnTypingKeystroke(); this._enableTypeAroundFakeCaretActivationUsingKeyboardArrows(); this._enableDeleteIntegration(); } @@ -194,12 +194,13 @@ export default class WidgetTypeAround extends Plugin { const editingView = editor.editing.view; // This is the main listener responsible for the "fake caret". - // Note: The priority must precede the default Widget class keydown handler. + // Note: The priority must precede the default Widget class keydown handler ("high") and the + // TableKeyboard keydown handler ("high + 1"). editingView.document.on( 'keydown', ( evt, domEventData ) => { if ( isArrowKeyCode( domEventData.keyCode ) ) { this._handleArrowKeyPress( evt, domEventData ); } - }, { priority: priorities.get( 'high' ) + 1 } ); + }, { priority: priorities.get( 'high' ) + 10 } ); // This listener makes sure the widget type around selection attribute will be gone from the model // selection as soon as the model range changes. This attribute only makes sense when a widget is selected @@ -449,9 +450,9 @@ export default class WidgetTypeAround extends Plugin { /** * Similar to the {@link #_enableInsertingParagraphsOnEnterKeypress}, it allows the user * to insert a paragraph next to a widget when the "fake caret" was activated using arrow - * keys but it responds to "unsafe keystrokes" instead of "enter". + * keys but it responds to "typing keystrokes" instead of "enter". * - * "Unsafe keystrokes" are keystrokes that insert new content into the document + * "Typing keystrokes" are keystrokes that insert new content into the document * like, for instance, letters ("a") or numbers ("4"). The "keydown" listener enabled by this method * will insert a new paragraph according to the "widget-type-around" model selection attribute * as the user simply starts typing, which creates the impression that the "fake caret" @@ -465,7 +466,7 @@ export default class WidgetTypeAround extends Plugin { * * @private */ - _enableInsertingParagraphsOnUnsafeKeystroke() { + _enableInsertingParagraphsOnTypingKeystroke() { const editor = this.editor; const editingView = editor.editing.view; const keyCodesHandledSomewhereElse = [ @@ -474,7 +475,8 @@ export default class WidgetTypeAround extends Plugin { keyCodes.backspace ]; - // Note: The priority must precede the default Widget class keydown handler. + // Note: The priority must precede the default Widget class keydown handler ("high") and the + // TableKeyboard keydown handler ("high + 1"). editingView.document.on( 'keydown', ( evt, domEventData ) => { // Don't handle enter/backspace/delete here. They are handled in dedicated listeners. if ( !keyCodesHandledSomewhereElse.includes( domEventData.keyCode ) && !isNonTypingKeystroke( domEventData ) ) { From 022cada79dbcc980baf936c4ab55b4e88ffc56d8 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 9 Jun 2020 16:16:48 +0200 Subject: [PATCH 49/69] Used a cached reference to a view widget in the WidgetTypeAround plugin to remove the CSS class responsible for the fake caret. --- .../src/widgettypearound/widgettypearound.js | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index bc646ca4d97..672f4b5533a 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -60,6 +60,23 @@ export default class WidgetTypeAround extends Plugin { return 'WidgetTypeAround'; } + /** + * @inheritDoc + */ + constructor( editor ) { + super( editor ); + + /** + * A reference to the editing view widget element that has the "fake caret" active + * on either side of it. It is later used to remove CSS classes associated with the "fake caret" + * when the widget no longer it. + * + * @private + * @member {module:engine/view/element~Element|null} + */ + this._currentFakeCaretViewWidget = null; + } + /** * @inheritDoc */ @@ -72,6 +89,13 @@ export default class WidgetTypeAround extends Plugin { this._enableDeleteIntegration(); } + /** + * @inheritDoc + */ + destroy() { + this._currentFakeCaretViewWidget = null; + } + /** * Inserts a new paragraph next to a widget element with the selection anchored in it. * @@ -212,7 +236,9 @@ export default class WidgetTypeAround extends Plugin { return; } - if ( !modelSelection.hasAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ) { + const typeAroundSelectionAttribute = modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); + + if ( !typeAroundSelectionAttribute ) { return; } @@ -222,12 +248,10 @@ export default class WidgetTypeAround extends Plugin { writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); } ); - // Also, if the range changes, get rid of CSS classes associated with the active ("fake horizontal caret") mode. - // There's no way to do that in the "selection" downcast dispatcher because it is executed too late. + // Also, if the range changes, get rid of CSS classes associated with the active ("fake horizontal caret") mode + // from the view widget. editingView.change( writer => { - const selectedViewElement = editingView.document.selection.getSelectedElement(); - - writer.removeClass( POSSIBLE_INSERTION_POSITIONS.map( positionToWidgetCssClass ), selectedViewElement ); + writer.removeClass( positionToWidgetCssClass( typeAroundSelectionAttribute ), this._currentFakeCaretViewWidget ); } ); } ); @@ -255,6 +279,10 @@ export default class WidgetTypeAround extends Plugin { } else { writer.removeClass( POSSIBLE_INSERTION_POSITIONS.map( positionToWidgetCssClass ), selectedViewElement ); } + + // Remember the view widget that got the "fake-caret" CSS class. This class should be removed ASAP when the + // selection changes + this._currentFakeCaretViewWidget = selectedViewElement; } ); this.listenTo( editor.ui.focusTracker, 'change:isFocused', ( evt, name, isFocused ) => { From c18a487cd152a714166899a7d056946b3d990c9a Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 9 Jun 2020 16:17:44 +0200 Subject: [PATCH 50/69] Update packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js Co-authored-by: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> --- .../ckeditor5-widget/src/widgettypearound/widgettypearound.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 672f4b5533a..c6ffa9eda24 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -255,7 +255,7 @@ export default class WidgetTypeAround extends Plugin { } ); } ); - // React to changes of the mode selection attribute made by the arrow keys listener. + // React to changes of the model selection attribute made by the arrow keys listener. // If the block widget is selected and the attribute changes, downcast the attribute to special // CSS classes associated with the active ("fake horizontal caret") mode of the widget. editor.editing.downcastDispatcher.on( 'selection', ( evt, data, conversionApi ) => { From ffc5b131109de8cadb357e280440d644e5737f65 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 9 Jun 2020 16:21:56 +0200 Subject: [PATCH 51/69] Docs. --- .../ckeditor5-widget/src/widgettypearound/widgettypearound.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index c6ffa9eda24..47824cf11c3 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -345,8 +345,8 @@ export default class WidgetTypeAround extends Plugin { } // If the selection had nowhere to go, let's leave the attribute as it was and pass through - // to the Widget plugin listener which will... in fact also do nothing. But this is no longer - // the problem of the WidgetTypeAround plugin. + // to the Widget plugin listener which will... in fact also do nothing. Other listeners like in the TableKeyboard + // plugin may want to handle it, though. But this is no longer the problem of the WidgetTypeAround plugin. } // ...and the keyboard arrow works against the value of the selection attribute... else { From a3de8bb5f577e3bb65e6c1513045db83cf741091 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 9 Jun 2020 16:23:46 +0200 Subject: [PATCH 52/69] Code refactoring: Don't look for nearest selection range if not laving the fake caret mode. --- .../src/widgettypearound/widgettypearound.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 47824cf11c3..ad3f530c7e2 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -333,13 +333,14 @@ export default class WidgetTypeAround extends Plugin { // If the selection already has the attribute... if ( typeAroundSelectionAttribute ) { const selectionPosition = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition(); - const nearestSelectionRange = schema.getNearestSelectionRange( selectionPosition, isForward ? 'forward' : 'backward' ); const isLeavingWidget = typeAroundSelectionAttribute === ( isForward ? 'after' : 'before' ); // ...and the keyboard arrow matches the value of the selection attribute... if ( isLeavingWidget ) { + const nearestRange = schema.getNearestSelectionRange( selectionPosition, isForward ? 'forward' : 'backward' ); + // ...and if there is some place for the selection to go to... - if ( nearestSelectionRange ) { + if ( nearestRange ) { // ...then just remove the attribute and let the default Widget plugin listener handle moving the selection. writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); } From 1936a53ed86b4074f06fc21ee925d4ff3fe6dc8e Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 9 Jun 2020 16:25:12 +0200 Subject: [PATCH 53/69] Update packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js Co-authored-by: Kuba Niegowski <1232187+niegowski@users.noreply.github.com> --- .../src/widgettypearound/widgettypearound.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index ad3f530c7e2..a6d21ddd729 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -361,11 +361,7 @@ export default class WidgetTypeAround extends Plugin { // If the selection didn't have the attribute, let's set it now according to the direction of the arrow // key press. This also means we cannot let the Widget plugin listener move the selection. else { - if ( isForward ) { - writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'after' ); - } else { - writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' ); - } + writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, isForward ? 'after' : 'before' ); shouldStopAndPreventDefault = true; } From b4aa47315bf9b70a968442671ae20cf855209113 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 9 Jun 2020 16:40:22 +0200 Subject: [PATCH 54/69] Code refactoring: Split WidgetTypeAround#_handleArrowKeyPress into two shorter sub-methods. --- .../src/widgettypearound/widgettypearound.js | 156 +++++++++++------- 1 file changed, 99 insertions(+), 57 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index a6d21ddd729..d2be658866a 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -327,76 +327,118 @@ export default class WidgetTypeAround extends Plugin { // Handle keyboard navigation when a type-around-compatible widget is currently selected. if ( isTypeAroundWidget( selectedViewElement, selectedModelElement, schema ) ) { - const typeAroundSelectionAttribute = modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); + shouldStopAndPreventDefault = this._handleArrowKeyPressOnSelectedWidget( isForward ); + } + // Handle keyboard arrow navigation when the selection is next to a type-around-compatible widget + // and the widget is about to be selected. + else if ( modelSelection.isCollapsed ) { + shouldStopAndPreventDefault = this._handleArrowKeyPressWhenSelectionNextToAWidget( isForward ); + } - model.change( writer => { - // If the selection already has the attribute... - if ( typeAroundSelectionAttribute ) { - const selectionPosition = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition(); - const isLeavingWidget = typeAroundSelectionAttribute === ( isForward ? 'after' : 'before' ); - - // ...and the keyboard arrow matches the value of the selection attribute... - if ( isLeavingWidget ) { - const nearestRange = schema.getNearestSelectionRange( selectionPosition, isForward ? 'forward' : 'backward' ); - - // ...and if there is some place for the selection to go to... - if ( nearestRange ) { - // ...then just remove the attribute and let the default Widget plugin listener handle moving the selection. - writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); - } - - // If the selection had nowhere to go, let's leave the attribute as it was and pass through - // to the Widget plugin listener which will... in fact also do nothing. Other listeners like in the TableKeyboard - // plugin may want to handle it, though. But this is no longer the problem of the WidgetTypeAround plugin. - } - // ...and the keyboard arrow works against the value of the selection attribute... - else { - // ...then remove the selection attribute but prevent default DOM actions - // and do not let the Widget plugin listener move the selection. This brings - // the widget back to the state, for instance, like if was selected using the mouse. + if ( shouldStopAndPreventDefault ) { + domEventData.preventDefault(); + evt.stop(); + } + } + + /** + * Handles the keyboard navigation on "keydown" when a widget is currently selected and activates or deactivates + * the "fake caret" for that widget, depending on the current value of the "widget-type-around" model + * selection attribute and the direction of the pressed arrow key. + * + * @private + * @param {Boolean} isForward `true` when the pressed arrow key was responsible for the forward model selection movement + * as in {@link module:utils/keyboard~isForwardArrowKeyCode}. + * @returns {Boolean} `true` when the key press was handled and no other keydown listener of the editor should + * process the event any further. `false` otherwise. + */ + _handleArrowKeyPressOnSelectedWidget( isForward ) { + const editor = this.editor; + const model = editor.model; + const schema = model.schema; + const modelSelection = model.document.selection; + const typeAroundSelectionAttribute = modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); + let shouldStopAndPreventDefault = false; + + model.change( writer => { + // If the selection already has the attribute... + if ( typeAroundSelectionAttribute ) { + const selectionPosition = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition(); + const isLeavingWidget = typeAroundSelectionAttribute === ( isForward ? 'after' : 'before' ); + + // ...and the keyboard arrow matches the value of the selection attribute... + if ( isLeavingWidget ) { + const nearestRange = schema.getNearestSelectionRange( selectionPosition, isForward ? 'forward' : 'backward' ); + + // ...and if there is some place for the selection to go to... + if ( nearestRange ) { + // ...then just remove the attribute and let the default Widget plugin listener handle moving the selection. writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); - shouldStopAndPreventDefault = true; } + + // If the selection had nowhere to go, let's leave the attribute as it was and pass through + // to the Widget plugin listener which will... in fact also do nothing. Other listeners like in the TableKeyboard + // plugin may want to handle it, though. But this is no longer the problem of the WidgetTypeAround plugin. } - // If the selection didn't have the attribute, let's set it now according to the direction of the arrow - // key press. This also means we cannot let the Widget plugin listener move the selection. + // ...and the keyboard arrow works against the value of the selection attribute... else { - writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, isForward ? 'after' : 'before' ); + // ...then remove the selection attribute but prevent default DOM actions + // and do not let the Widget plugin listener move the selection. This brings + // the widget back to the state, for instance, like if was selected using the mouse. + writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); shouldStopAndPreventDefault = true; } - } ); - } - // Handle keyboard arrow navigation when the selection is next to a type-around-compatible widget - // and the widget is about to be selected. - // - // This code mirrors the implementation from the Widget plugin but also adds the selection attribute. - // Unfortunately, there's no safe way to let the Widget plugin do the selection part first - // and then just set the selection attribute here in the WidgetTypeAround plugin. This is why - // this code must duplicate some from the Widget plugin. - else if ( modelSelection.isCollapsed ) { - const widgetPlugin = editor.plugins.get( 'Widget' ); - - // This is the widget the selection is about to be set on. - const modelElementNextToSelection = widgetPlugin._getObjectElementNextToSelection( isForward ); - const viewElementNextToSelection = editor.editing.mapper.toViewElement( modelElementNextToSelection ); - - if ( isTypeAroundWidget( viewElementNextToSelection, modelElementNextToSelection, schema ) ) { - model.change( writer => { - widgetPlugin._setSelectionOverElement( modelElementNextToSelection ); - writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, isForward ? 'before' : 'after' ); - } ); + } + // If the selection didn't have the attribute, let's set it now according to the direction of the arrow + // key press. This also means we cannot let the Widget plugin listener move the selection. + else { + writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, isForward ? 'after' : 'before' ); - // The change() block above does the same job as the Widget plugin. The event can - // be safely canceled. shouldStopAndPreventDefault = true; } - } + } ); - if ( shouldStopAndPreventDefault ) { - domEventData.preventDefault(); - evt.stop(); + return shouldStopAndPreventDefault; + } + + /** + * Handles the keyboard navigation on "keydown" when **no** widget is selected but the selection is **directly** next + * to one and upon the "fake caret" should become active for this widget upon arrow key press + * (AKA entering/selecting the widget). + * + * **Note**: This code mirrors the implementation from the Widget plugin but also adds the selection attribute. + * Unfortunately, there's no safe way to let the Widget plugin do the selection part first and then just set the + * selection attribute here in the WidgetTypeAround plugin. This is why this code must duplicate some from the Widget plugin. + * + * @private + * @param {Boolean} isForward `true` when the pressed arrow key was responsible for the forward model selection movement + * as in {@link module:utils/keyboard~isForwardArrowKeyCode}. + * @returns {Boolean} `true` when the key press was handled and no other keydown listener of the editor should + * process the event any further. `false` otherwise. + */ + _handleArrowKeyPressWhenSelectionNextToAWidget( isForward ) { + const editor = this.editor; + const model = editor.model; + const schema = model.schema; + const widgetPlugin = editor.plugins.get( 'Widget' ); + + // This is the widget the selection is about to be set on. + const modelElementNextToSelection = widgetPlugin._getObjectElementNextToSelection( isForward ); + const viewElementNextToSelection = editor.editing.mapper.toViewElement( modelElementNextToSelection ); + + if ( isTypeAroundWidget( viewElementNextToSelection, modelElementNextToSelection, schema ) ) { + model.change( writer => { + widgetPlugin._setSelectionOverElement( modelElementNextToSelection ); + writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, isForward ? 'before' : 'after' ); + } ); + + // The change() block above does the same job as the Widget plugin. The event can + // be safely canceled. + return true; } + + return false; } /** From 56c857f0b29d3773b8556c3e089dc34acd246c13 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 9 Jun 2020 16:43:25 +0200 Subject: [PATCH 55/69] Code refactoring. --- .../ckeditor5-widget/src/widgettypearound/widgettypearound.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index d2be658866a..8111fb07bd9 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -574,7 +574,7 @@ export default class WidgetTypeAround extends Plugin { // This listener handles only these cases when the "fake caret" is active. if ( !typeAroundSelectionAttributeValue ) { - return false; + return; } const direction = domEventData.direction; From f265208334eb644f8bd786046fa013c57167b86c Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 9 Jun 2020 18:12:33 +0200 Subject: [PATCH 56/69] Improved delete and backspace handling around blocks widgets when empty elements precede or follow a widget. --- .../src/widgettypearound/widgettypearound.js | 102 ++++++++++--- .../widgettypearound/widgettypearound.js | 138 ++++++++++++++++++ 2 files changed, 223 insertions(+), 17 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 8111fb07bd9..245b60356fa 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -580,30 +580,69 @@ export default class WidgetTypeAround extends Plugin { const direction = domEventData.direction; const selectedModelWidget = model.document.selection.getSelectedElement(); - if ( typeAroundSelectionAttributeValue === 'before' ) { - if ( direction === 'backward' ) { - const range = schema.getNearestSelectionRange( model.createPositionBefore( selectedModelWidget ), direction ); + const isFakeCaretBefore = typeAroundSelectionAttributeValue === 'before'; + const isForwardDelete = direction == 'forward'; - if ( range ) { + const shouldDeleteEntireWidget = ( isFakeCaretBefore && isForwardDelete ) || ( !isFakeCaretBefore && !isForwardDelete ); + const shouldTryDeleteContentBeforeWidget = isFakeCaretBefore && !isForwardDelete; + const shouldTryDeleteContentAfterWidget = !isFakeCaretBefore && isForwardDelete; + + if ( shouldDeleteEntireWidget ) { + editor.execute( 'delete', { + selection: model.createSelection( selectedModelWidget, 'on' ) + } ); + } + + if ( shouldTryDeleteContentBeforeWidget ) { + const range = schema.getNearestSelectionRange( model.createPositionBefore( selectedModelWidget ), direction ); + + if ( range ) { + const deepestEmptyRangeAncestor = getDeepestEmptyPositionAncestor( range.start ); + + // Handle a case when there's an empty document tree branch before the widget that should be deleted. + // + // [] + // + // Note: Range is collapsed, so it does not matter if this is start or end. + if ( deepestEmptyRangeAncestor ) { + model.deleteContent( model.createSelection( deepestEmptyRangeAncestor, 'on' ), { + doNotAutoparagraph: true + } ); + } + // Handle a case when there's a non-empty document tree branch before the widget. + // + // bar[] -> ba[] + // + else { model.change( writer => { writer.setSelection( range ); editor.execute( 'delete' ); } ); } - } else { - editor.execute( 'delete', { - selection: model.createSelection( selectedModelWidget, 'on' ) - } ); } - } else { - if ( direction === 'backward' ) { - editor.execute( 'delete', { - selection: model.createSelection( selectedModelWidget, 'on' ) - } ); - } else { - const range = schema.getNearestSelectionRange( model.createPositionAfter( selectedModelWidget ), direction ); - - if ( range ) { + } + + if ( shouldTryDeleteContentAfterWidget ) { + const range = schema.getNearestSelectionRange( model.createPositionAfter( selectedModelWidget ), direction ); + + if ( range ) { + const deepestEmptyRangeAncestor = getDeepestEmptyPositionAncestor( range.start ); + + // Handle a case when there's an empty document tree branch after the widget that should be deleted. + // + // [] + // + // Note: Range is collapsed, so it does not matter if this is start or end. + if ( deepestEmptyRangeAncestor ) { + model.deleteContent( model.createSelection( deepestEmptyRangeAncestor, 'on' ), { + doNotAutoparagraph: true + } ); + } + // Handle a case when there's a non-empty document tree branch after the widget. + // + // []bar -> []ar + // + else { model.change( writer => { writer.setSelection( range ); editor.execute( 'forwardDelete' ); @@ -682,3 +721,32 @@ function injectFakeCaret( wrapperDomElement ) { wrapperDomElement.appendChild( caretTemplate.render() ); } + +// Returns the ancestor of a position closest to the root which is empty. For instance, +// for a position in ``: +// +// abc[] +// +// it returns ``. +// +// @param {module:engine/model/position~Position} position +// @returns {module:engine/model/element~Element|null} +function getDeepestEmptyPositionAncestor( position ) { + const firstPositionParent = position.parent; + + if ( !firstPositionParent.isEmpty ) { + return null; + } + + let deepestEmptyAncestor = firstPositionParent; + + for ( const ancestor of firstPositionParent.getAncestors().reverse() ) { + if ( ancestor.childCount > 1 || ancestor.is( 'rootElement' ) ) { + break; + } + + deepestEmptyAncestor = ancestor; + } + + return deepestEmptyAncestor; +} diff --git a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js index 2a5b143572b..0742830eab8 100644 --- a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js @@ -894,6 +894,76 @@ describe( 'WidgetTypeAround', () => { sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); } ); + it( 'should delete an empty paragraph before a widget if the "fake caret" is also before the widget', () => { + setModelData( editor.model, '[]' ); + + fireKeyboardEvent( 'arrowleft' ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + fireDeleteEvent(); + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should delete an empty document tree branch before a widget if the "fake caret" is also before the widget', () => { + setModelData( editor.model, '
[]' ); + + fireKeyboardEvent( 'arrowleft' ); + + expect( getModelData( model ) ).to.equal( + '
' + + '' + + '
' + + '[]' + ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + fireDeleteEvent(); + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should delete an empty document tree sub-branch before a widget if the "fake caret" is also before the widget', () => { + setModelData( editor.model, + '
' + + 'foo' + + '' + + '
' + + '[]' + ); + + fireKeyboardEvent( 'arrowleft' ); + + expect( getModelData( model ) ).to.equal( + '
' + + 'foo' + + '' + + '
' + + '[]' + ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + fireDeleteEvent(); + expect( getModelData( model ) ).to.equal( + '
' + + 'foo' + + '
' + + '[]' + ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + it( 'should do nothing if the "fake caret" is before the widget but there is nothing to delete there', () => { setModelData( editor.model, '[]' ); @@ -979,6 +1049,74 @@ describe( 'WidgetTypeAround', () => { sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); } ); + it( 'should delete an empty paragraph after a widget if the "fake caret" is also after the widget', () => { + setModelData( editor.model, '[]' ); + + fireKeyboardEvent( 'arrowright' ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + + fireDeleteEvent( true ); + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should delete an empty document tree branch after a widget if the "fake caret" is also after the widget', () => { + setModelData( editor.model, '[]
' ); + + fireKeyboardEvent( 'arrowright' ); + + expect( getModelData( model ) ).to.equal( + '[]' + + '
' + ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + + fireDeleteEvent( true ); + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + + it( 'should delete an empty document tree sub-branch after a widget if the "fake caret" is also after the widget', () => { + setModelData( editor.model, + '[]' + + '
' + + '' + + 'foo' + + '
' + ); + + fireKeyboardEvent( 'arrowright' ); + + expect( getModelData( model ) ).to.equal( + '[]' + + '
' + + '' + + 'foo' + + '
' + ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + + fireDeleteEvent( true ); + expect( getModelData( model ) ).to.equal( + '[]' + + '
' + + 'foo' + + '
' + ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + it( 'should do nothing if the "fake caret" is after the widget but there is nothing to delete there', () => { setModelData( editor.model, '[]' ); From ab4144016036e4deee264f688c20652fa569c3ef Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 9 Jun 2020 18:24:30 +0200 Subject: [PATCH 57/69] Tests: Used proper key codes for Shift, Ctrl and Alt instead of internal masks in isNonTypingKeystroke() tests. --- .../tests/utils/injectunsafekeystrokeshandling.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js b/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js index f483eda5e78..a283a830a31 100644 --- a/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js +++ b/packages/ckeditor5-typing/tests/utils/injectunsafekeystrokeshandling.js @@ -51,9 +51,9 @@ describe( 'unsafe keystroke handling utils', () => { it( 'should return "true" for other safe keystrokes', () => { expect( isNonTypingKeystroke( { keyCode: keyCodes.tab } ), 'Tab' ).to.be.true; - expect( isNonTypingKeystroke( { keyCode: keyCodes.shift } ), 'Shift' ).to.be.true; - expect( isNonTypingKeystroke( { keyCode: keyCodes.ctrl } ), 'Ctrl' ).to.be.true; - expect( isNonTypingKeystroke( { keyCode: keyCodes.alt } ), 'Alt' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 16 } ), 'Shift' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 17 } ), 'Ctrl' ).to.be.true; + expect( isNonTypingKeystroke( { keyCode: 18 } ), 'Alt' ).to.be.true; expect( isNonTypingKeystroke( { keyCode: 19 } ), 'Pause' ).to.be.true; expect( isNonTypingKeystroke( { keyCode: 20 } ), 'CapsLock' ).to.be.true; expect( isNonTypingKeystroke( { keyCode: keyCodes.esc } ), 'Escape' ).to.be.true; From 771898e74ae97f1348e7bb3409dd9619bf0049f9 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 10 Jun 2020 10:12:03 +0200 Subject: [PATCH 58/69] Tests: Added a test to check if all 4 arrow keys activate the WidgetTypeAround fake caret. --- .../widgettypearound/widgettypearound.js | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js index 0742830eab8..2f2fdca4008 100644 --- a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js @@ -615,6 +615,30 @@ describe( 'WidgetTypeAround', () => { } ); } ); + it( 'should activate and deactivate the "fake caret" using all 4 arrow keys', () => { + setModelData( editor.model, 'foo[]' ); + + fireKeyboardEvent( 'arrowright' ); + + expect( getModelData( model ) ).to.equal( 'foo[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + fireKeyboardEvent( 'arrowdown' ); + + expect( getModelData( model ) ).to.equal( 'foo[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + fireKeyboardEvent( 'arrowup' ); + + expect( getModelData( model ) ).to.equal( 'foo[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + fireKeyboardEvent( 'arrowleft' ); + + expect( getModelData( model ) ).to.equal( 'foo[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + } ); + it( 'should quit the "fake caret" mode when the editor loses focus', () => { editor.ui.focusTracker.isFocused = true; @@ -813,7 +837,7 @@ describe( 'WidgetTypeAround', () => { } ); } ); - describe( 'on typing an "unsafe" character when the "fake caret" is activated ', () => { + describe( 'on keydown of a "typing" character when the "fake caret" is activated ', () => { it( 'should insert a character inside a new paragraph before a widget if the caret was "before" it', () => { setModelData( editor.model, '[]' ); From 289dac809b6c23bdcb4a200c74bfc9f5f549808d Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 10 Jun 2020 10:17:01 +0200 Subject: [PATCH 59/69] Tests: Added tests for WidgetTypeAround using an arrow with a Shift modifier. --- .../widgettypearound/widgettypearound.js | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js index 2f2fdca4008..93535d7c1a4 100644 --- a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js @@ -357,6 +357,18 @@ describe( 'WidgetTypeAround', () => { sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); } ); + it( 'should activate if an arrow key is pressed along with Shift', () => { + setModelData( editor.model, 'foo[]' ); + + fireKeyboardEvent( 'arrowright', { shiftKey: true } ); + + expect( getModelData( model ) ).to.equal( 'foo[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + it( 'should not activate when the selection is before the widget but the non-arrow key was pressed', () => { setModelData( editor.model, 'foo[]' ); @@ -508,6 +520,26 @@ describe( 'WidgetTypeAround', () => { sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); } ); + it( 'should deactivate if an arrow key is pressed along with Shift', () => { + setModelData( editor.model, 'foo[]' ); + + fireKeyboardEvent( 'arrowleft', { shiftKey: true } ); + + expect( getModelData( model ) ).to.equal( 'foo[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + + fireKeyboardEvent( 'arrowleft', { shiftKey: true } ); + + expect( getModelData( model ) ).to.equal( 'foo[]' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + + sinon.assert.calledOnce( eventInfoStub.stop ); + sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); + } ); + it( 'should not deactivate when the widget is selected and the navigation is backward but there is nowhere to go', () => { setModelData( editor.model, '[]' ); From 09b57f856cb62bc2dbc9287038379e18564a2ce7 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 10 Jun 2020 10:31:10 +0200 Subject: [PATCH 60/69] Integrated WidgetTypeAround feature with the widget resize UI. --- .../theme/ckeditor5-widget/widgettypearound.css | 9 +++++++++ packages/ckeditor5-widget/tests/manual/type-around.js | 11 ++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css index 804c50356f8..3dcdd7f5bf4 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css @@ -167,6 +167,15 @@ &.ck-widget_selected.ck-widget_with-selection-handle > .ck-widget__selection-handle { opacity: 0 } + + /* + * Fake horizontal caret integration with the resize UI. When the caret is visible, simply + * hide the resize UI because it creates too much noise. It can be visible when the user + * hovers the widget, though. + */ + &.ck-widget_selected.ck-widget_with-resizer:not(:hover) > .ck-widget__resizer { + opacity: 0 + } } } diff --git a/packages/ckeditor5-widget/tests/manual/type-around.js b/packages/ckeditor5-widget/tests/manual/type-around.js index a51ae2a039e..6de1fc375c8 100644 --- a/packages/ckeditor5-widget/tests/manual/type-around.js +++ b/packages/ckeditor5-widget/tests/manual/type-around.js @@ -11,6 +11,7 @@ import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalli import MediaEmbed from '@ckeditor/ckeditor5-media-embed/src/mediaembed'; import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties'; import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties'; +import ImageResize from '@ckeditor/ckeditor5-image/src/imageresize'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; @@ -98,7 +99,15 @@ document.querySelector( '#toggleReadOnly' ).addEventListener( 'click', () => { ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ ArticlePluginSet, HorizontalLine, InlineWidget, MediaEmbed, TableProperties, TableCellProperties ], + plugins: [ + ArticlePluginSet, + HorizontalLine, + InlineWidget, + MediaEmbed, + TableProperties, + TableCellProperties, + ImageResize + ], toolbar: [ 'heading', '|', From 3a531dd321ad4b05784a6d9b6dbe62ebffc0159e Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 10 Jun 2020 11:03:22 +0200 Subject: [PATCH 61/69] Code refactoring: Made WidgetTypeAround#_insertParagraph accept a model element instead of a view element. --- .../src/widgettypearound/widgettypearound.js | 18 +++++++++--------- .../widgettypearound/widgettypearound.js | 19 ++++++++++--------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 245b60356fa..da1e37ff2ad 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -103,13 +103,12 @@ export default class WidgetTypeAround extends Plugin { * the viewport to the selection in the inserted paragraph. * * @protected - * @param {module:engine/view/element~Element} widgetViewElement The view widget element next to which a paragraph is inserted. + * @param {module:engine/model/element~Element} widgetModelElement The model widget element next to which a paragraph is inserted. * @param {'before'|'after'} position The position where the paragraph is inserted. Either `'before'` or `'after'` the widget. */ - _insertParagraph( widgetViewElement, position ) { + _insertParagraph( widgetModelElement, position ) { const editor = this.editor; const editingView = editor.editing.view; - const widgetModelElement = editor.editing.mapper.toModelElement( widgetViewElement ); let modelPosition; if ( position === 'before' ) { @@ -141,16 +140,16 @@ export default class WidgetTypeAround extends Plugin { _insertParagraphAccordingToSelectionAttribute() { const editor = this.editor; const model = editor.model; - const editingView = editor.editing.view; - const typeAroundSelectionAttributeValue = model.document.selection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); + const modelSelection = model.document.selection; + const typeAroundSelectionAttributeValue = modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); if ( !typeAroundSelectionAttributeValue ) { return false; } - const selectedViewElement = editingView.document.selection.getSelectedElement(); + const selectedModelElement = modelSelection.getSelectedElement(); - this._insertParagraph( selectedViewElement, typeAroundSelectionAttributeValue ); + this._insertParagraph( selectedModelElement, typeAroundSelectionAttributeValue ); return true; } @@ -461,8 +460,9 @@ export default class WidgetTypeAround extends Plugin { const buttonPosition = getTypeAroundButtonPosition( button ); const widgetViewElement = getClosestWidgetViewElement( button, editingView.domConverter ); + const widgetModelElement = editor.editing.mapper.toModelElement( widgetViewElement ); - this._insertParagraph( widgetViewElement, buttonPosition ); + this._insertParagraph( widgetModelElement, buttonPosition ); domEventData.preventDefault(); evt.stop(); @@ -502,7 +502,7 @@ export default class WidgetTypeAround extends Plugin { // Then, if there is no selection attribute associated with the "fake caret", check if the widget // simply is selected and create a new paragraph according to the keystroke (Shift+)Enter. else if ( isTypeAroundWidget( selectedViewElement, selectedModelElement, schema ) ) { - this._insertParagraph( selectedViewElement, domEventData.isSoft ? 'before' : 'after' ); + this._insertParagraph( selectedModelElement, domEventData.isSoft ? 'before' : 'after' ); wasHandled = true; } diff --git a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js index 93535d7c1a4..73a6919e18a 100644 --- a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js @@ -18,7 +18,7 @@ import { setData as setModelData, getData as getModelData } from '@ckeditor/cked import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard'; describe( 'WidgetTypeAround', () => { - let element, plugin, editor, editingView, viewDocument, viewRoot; + let element, plugin, editor, editingView, viewDocument, modelRoot, viewRoot; beforeEach( async () => { element = global.document.createElement( 'div' ); @@ -35,6 +35,7 @@ describe( 'WidgetTypeAround', () => { editingView = editor.editing.view; viewDocument = editingView.document; viewRoot = viewDocument.getRoot(); + modelRoot = editor.model.document.getRoot(); plugin = editor.plugins.get( WidgetTypeAround ); } ); @@ -64,10 +65,10 @@ describe( 'WidgetTypeAround', () => { it( 'should execute the "insertParagraph" command when inserting a paragraph before the widget', () => { setModelData( editor.model, '' ); - plugin._insertParagraph( viewRoot.getChild( 0 ), 'before' ); + plugin._insertParagraph( modelRoot.getChild( 0 ), 'before' ); const spyExecutePosition = executeSpy.firstCall.args[ 1 ].position; - const positionBeforeWidget = editor.model.createPositionBefore( editor.model.document.getRoot().getChild( 0 ) ); + const positionBeforeWidget = editor.model.createPositionBefore( modelRoot.getChild( 0 ) ); sinon.assert.calledOnce( executeSpy ); sinon.assert.calledWith( executeSpy, 'insertParagraph' ); @@ -80,10 +81,10 @@ describe( 'WidgetTypeAround', () => { it( 'should execute the "insertParagraph" command when inserting a paragraph after the widget', () => { setModelData( editor.model, '' ); - plugin._insertParagraph( viewRoot.getChild( 0 ), 'after' ); + plugin._insertParagraph( modelRoot.getChild( 0 ), 'after' ); const spyExecutePosition = executeSpy.firstCall.args[ 1 ].position; - const positionAfterWidget = editor.model.createPositionAfter( editor.model.document.getRoot().getChild( 0 ) ); + const positionAfterWidget = editor.model.createPositionAfter( modelRoot.getChild( 0 ) ); sinon.assert.calledOnce( executeSpy ); sinon.assert.calledWith( executeSpy, 'insertParagraph' ); @@ -98,7 +99,7 @@ describe( 'WidgetTypeAround', () => { setModelData( editor.model, '' ); - plugin._insertParagraph( viewRoot.getChild( 0 ), 'after' ); + plugin._insertParagraph( modelRoot.getChild( 0 ), 'after' ); sinon.assert.calledOnce( spy ); } ); @@ -108,7 +109,7 @@ describe( 'WidgetTypeAround', () => { setModelData( editor.model, '' ); - plugin._insertParagraph( viewRoot.getChild( 0 ), 'after' ); + plugin._insertParagraph( modelRoot.getChild( 0 ), 'after' ); sinon.assert.calledOnce( spy ); } ); @@ -222,7 +223,7 @@ describe( 'WidgetTypeAround', () => { viewDocument.fire( eventInfo, domEventDataMock ); sinon.assert.calledOnce( typeAroundSpy ); - sinon.assert.calledWithExactly( typeAroundSpy, viewRoot.getChild( 1 ), 'before' ); + sinon.assert.calledWithExactly( typeAroundSpy, modelRoot.getChild( 1 ), 'before' ); sinon.assert.calledOnce( preventDefaultSpy ); sinon.assert.calledOnce( stopSpy ); } ); @@ -265,7 +266,7 @@ describe( 'WidgetTypeAround', () => { viewDocument.fire( eventInfo, domEventDataMock ); sinon.assert.calledOnce( typeAroundSpy ); - sinon.assert.calledWithExactly( typeAroundSpy, viewRoot.getChild( 0 ), 'after' ); + sinon.assert.calledWithExactly( typeAroundSpy, modelRoot.getChild( 0 ), 'after' ); sinon.assert.calledOnce( preventDefaultSpy ); sinon.assert.calledOnce( stopSpy ); } ); From 241bfc1084beb1c0be4f8c657040603d98e78877 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 10 Jun 2020 11:08:20 +0200 Subject: [PATCH 62/69] Code refactoring in the WidgetTypeAround plugin. --- .../src/widgettypearound/widgettypearound.js | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index da1e37ff2ad..6d8d38b43bc 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -354,7 +354,6 @@ export default class WidgetTypeAround extends Plugin { _handleArrowKeyPressOnSelectedWidget( isForward ) { const editor = this.editor; const model = editor.model; - const schema = model.schema; const modelSelection = model.document.selection; const typeAroundSelectionAttribute = modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); let shouldStopAndPreventDefault = false; @@ -362,28 +361,17 @@ export default class WidgetTypeAround extends Plugin { model.change( writer => { // If the selection already has the attribute... if ( typeAroundSelectionAttribute ) { - const selectionPosition = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition(); const isLeavingWidget = typeAroundSelectionAttribute === ( isForward ? 'after' : 'before' ); - // ...and the keyboard arrow matches the value of the selection attribute... - if ( isLeavingWidget ) { - const nearestRange = schema.getNearestSelectionRange( selectionPosition, isForward ? 'forward' : 'backward' ); - - // ...and if there is some place for the selection to go to... - if ( nearestRange ) { - // ...then just remove the attribute and let the default Widget plugin listener handle moving the selection. - writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); - } - - // If the selection had nowhere to go, let's leave the attribute as it was and pass through - // to the Widget plugin listener which will... in fact also do nothing. Other listeners like in the TableKeyboard - // plugin may want to handle it, though. But this is no longer the problem of the WidgetTypeAround plugin. - } - // ...and the keyboard arrow works against the value of the selection attribute... - else { - // ...then remove the selection attribute but prevent default DOM actions - // and do not let the Widget plugin listener move the selection. This brings - // the widget back to the state, for instance, like if was selected using the mouse. + // If the keyboard arrow works against the value of the selection attribute... + // then remove the selection attribute but prevent default DOM actions + // and do not let the Widget plugin listener move the selection. This brings + // the widget back to the state, for instance, like if was selected using the mouse. + // + // **Note**: If leaving the widget when the "fake caret" is active, then the default + // Widget handler will change the selection and, in turn, this will automatically discard + // the selection attribute. + if ( !isLeavingWidget ) { writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); shouldStopAndPreventDefault = true; From d77595e6a2c5e27cc91dbdcf034127d507f51f22 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 10 Jun 2020 11:14:53 +0200 Subject: [PATCH 63/69] Code refactoring in the WidgetTypeAround plugin. --- .../src/widgettypearound/widgettypearound.js | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 6d8d38b43bc..c594468b29d 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -570,18 +570,13 @@ export default class WidgetTypeAround extends Plugin { const isFakeCaretBefore = typeAroundSelectionAttributeValue === 'before'; const isForwardDelete = direction == 'forward'; - - const shouldDeleteEntireWidget = ( isFakeCaretBefore && isForwardDelete ) || ( !isFakeCaretBefore && !isForwardDelete ); - const shouldTryDeleteContentBeforeWidget = isFakeCaretBefore && !isForwardDelete; - const shouldTryDeleteContentAfterWidget = !isFakeCaretBefore && isForwardDelete; + const shouldDeleteEntireWidget = isFakeCaretBefore === isForwardDelete; if ( shouldDeleteEntireWidget ) { editor.execute( 'delete', { selection: model.createSelection( selectedModelWidget, 'on' ) } ); - } - - if ( shouldTryDeleteContentBeforeWidget ) { + } else if ( !isForwardDelete ) { const range = schema.getNearestSelectionRange( model.createPositionBefore( selectedModelWidget ), direction ); if ( range ) { @@ -608,9 +603,7 @@ export default class WidgetTypeAround extends Plugin { } ); } } - } - - if ( shouldTryDeleteContentAfterWidget ) { + } else { const range = schema.getNearestSelectionRange( model.createPositionAfter( selectedModelWidget ), direction ); if ( range ) { From 682402ffeafb818150ceb429aed1696274e94c70 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 10 Jun 2020 11:24:50 +0200 Subject: [PATCH 64/69] Code refactoring in the WidgetTypeAround feature. --- .../src/widgettypearound/widgettypearound.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index c594468b29d..13532d6423b 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -580,7 +580,7 @@ export default class WidgetTypeAround extends Plugin { const range = schema.getNearestSelectionRange( model.createPositionBefore( selectedModelWidget ), direction ); if ( range ) { - const deepestEmptyRangeAncestor = getDeepestEmptyPositionAncestor( range.start ); + const deepestEmptyRangeAncestor = getDeepestEmptyPositionAncestor( schema, range.start ); // Handle a case when there's an empty document tree branch before the widget that should be deleted. // @@ -607,7 +607,7 @@ export default class WidgetTypeAround extends Plugin { const range = schema.getNearestSelectionRange( model.createPositionAfter( selectedModelWidget ), direction ); if ( range ) { - const deepestEmptyRangeAncestor = getDeepestEmptyPositionAncestor( range.start ); + const deepestEmptyRangeAncestor = getDeepestEmptyPositionAncestor( schema, range.start ); // Handle a case when there's an empty document tree branch after the widget that should be deleted. // @@ -710,9 +710,10 @@ function injectFakeCaret( wrapperDomElement ) { // // it returns ``. // +// @param {module:engine/model/schema~Schema} schema // @param {module:engine/model/position~Position} position // @returns {module:engine/model/element~Element|null} -function getDeepestEmptyPositionAncestor( position ) { +function getDeepestEmptyPositionAncestor( schema, position ) { const firstPositionParent = position.parent; if ( !firstPositionParent.isEmpty ) { @@ -721,8 +722,8 @@ function getDeepestEmptyPositionAncestor( position ) { let deepestEmptyAncestor = firstPositionParent; - for ( const ancestor of firstPositionParent.getAncestors().reverse() ) { - if ( ancestor.childCount > 1 || ancestor.is( 'rootElement' ) ) { + for ( const ancestor of firstPositionParent.getAncestors( { parentFirst: true } ) ) { + if ( ancestor.childCount > 1 || schema.isLimit( ancestor ) ) { break; } From db1c036ea767c61ffac49981cded48c88cda21a6 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 10 Jun 2020 12:08:40 +0200 Subject: [PATCH 65/69] Code refactoring: Use a cached model widget element to remove the type around classes instead of a view widget element. --- .../src/widgettypearound/widgettypearound.js | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 13532d6423b..5d2a4dcc34f 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -67,14 +67,14 @@ export default class WidgetTypeAround extends Plugin { super( editor ); /** - * A reference to the editing view widget element that has the "fake caret" active + * A reference to the editing model widget element that has the "fake caret" active * on either side of it. It is later used to remove CSS classes associated with the "fake caret" * when the widget no longer it. * * @private - * @member {module:engine/view/element~Element|null} + * @member {module:engine/model/element~Element|null} */ - this._currentFakeCaretViewWidget = null; + this._currentFakeCaretModelWidget = null; } /** @@ -93,7 +93,7 @@ export default class WidgetTypeAround extends Plugin { * @inheritDoc */ destroy() { - this._currentFakeCaretViewWidget = null; + this._currentFakeCaretModelWidget = null; } /** @@ -246,12 +246,6 @@ export default class WidgetTypeAround extends Plugin { editor.model.change( writer => { writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); } ); - - // Also, if the range changes, get rid of CSS classes associated with the active ("fake horizontal caret") mode - // from the view widget. - editingView.change( writer => { - writer.removeClass( positionToWidgetCssClass( typeAroundSelectionAttribute ), this._currentFakeCaretViewWidget ); - } ); } ); // React to changes of the model selection attribute made by the arrow keys listener. @@ -259,6 +253,18 @@ export default class WidgetTypeAround extends Plugin { // CSS classes associated with the active ("fake horizontal caret") mode of the widget. editor.editing.downcastDispatcher.on( 'selection', ( evt, data, conversionApi ) => { const writer = conversionApi.writer; + + if ( this._currentFakeCaretModelWidget ) { + const selectedViewElement = conversionApi.mapper.toViewElement( this._currentFakeCaretModelWidget ); + + if ( selectedViewElement ) { + // Get rid of CSS classes associated with the active ("fake horizontal caret") mode from the view widget. + writer.removeClass( POSSIBLE_INSERTION_POSITIONS.map( positionToWidgetCssClass ), selectedViewElement ); + + this._currentFakeCaretModelWidget = null; + } + } + const selectedModelElement = data.selection.getSelectedElement(); if ( !selectedModelElement ) { @@ -273,15 +279,15 @@ export default class WidgetTypeAround extends Plugin { const typeAroundSelectionAttribute = data.selection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ); - if ( typeAroundSelectionAttribute ) { - writer.addClass( positionToWidgetCssClass( typeAroundSelectionAttribute ), selectedViewElement ); - } else { - writer.removeClass( POSSIBLE_INSERTION_POSITIONS.map( positionToWidgetCssClass ), selectedViewElement ); + if ( !typeAroundSelectionAttribute ) { + return; } + writer.addClass( positionToWidgetCssClass( typeAroundSelectionAttribute ), selectedViewElement ); + // Remember the view widget that got the "fake-caret" CSS class. This class should be removed ASAP when the // selection changes - this._currentFakeCaretViewWidget = selectedViewElement; + this._currentFakeCaretModelWidget = selectedModelElement; } ); this.listenTo( editor.ui.focusTracker, 'change:isFocused', ( evt, name, isFocused ) => { From a2538825d851b9b754de1eb83de4fcc0856c38b0 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 10 Jun 2020 12:41:23 +0200 Subject: [PATCH 66/69] Improved some visual aspects of the widget when the fake caret is visible. --- .../ckeditor5-widget/widgettypearound.css | 20 ++++++++++++++++--- .../theme/widgettypearound.css | 16 ++++++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css index 3dcdd7f5bf4..5b19ef07d59 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-widget/widgettypearound.css @@ -152,6 +152,15 @@ &.ck-widget_type-around_show-fake-caret_before, &.ck-widget_type-around_show-fake-caret_after { + /* + * When the "fake caret" is visible we simulate that the widget is not selected + * (despite being physically selected), so the outline color should be for the + * unselected widget. + */ + &.ck-widget_selected:hover { + outline-color: var(--ck-color-widget-hover-border); + } + /* * Styles of the type around buttons when the "fake caret" is blinking (e.g. upon keyboard navigation). * In this state, the type around buttons would collide with the fake carets so they should disappear. @@ -164,8 +173,13 @@ * Fake horizontal caret integration with the selection handle. When the caret is visible, simply * hide the handle because it intersects with the caret (and does not make much sense anyway). */ - &.ck-widget_selected.ck-widget_with-selection-handle > .ck-widget__selection-handle { - opacity: 0 + &.ck-widget_with-selection-handle { + &.ck-widget_selected, + &.ck-widget_selected:hover { + & > .ck-widget__selection-handle { + opacity: 0 + } + } } /* @@ -173,7 +187,7 @@ * hide the resize UI because it creates too much noise. It can be visible when the user * hovers the widget, though. */ - &.ck-widget_selected.ck-widget_with-resizer:not(:hover) > .ck-widget__resizer { + &.ck-widget_selected.ck-widget_with-resizer > .ck-widget__resizer { opacity: 0 } } diff --git a/packages/ckeditor5-widget/theme/widgettypearound.css b/packages/ckeditor5-widget/theme/widgettypearound.css index 67639f3b1ee..34d4f0f709d 100644 --- a/packages/ckeditor5-widget/theme/widgettypearound.css +++ b/packages/ckeditor5-widget/theme/widgettypearound.css @@ -57,18 +57,28 @@ /* * Styles for the horizontal "fake caret" which is displayed when the user navigates using the keyboard. */ - & .ck-widget__type-around__fake-caret { + & > .ck-widget__type-around > .ck-widget__type-around__fake-caret { display: none; position: absolute; left: 0; right: 0; } + /* + * When the widget is hovered the "fake caret" would normally be narrower than the + * extra outline displayed around the widget. Let's extend the "fake caret" to match + * the full width of the widget. + */ + &:hover > .ck-widget__type-around > .ck-widget__type-around__fake-caret { + left: calc( -1 * var(--ck-widget-outline-thickness) ); + right: calc( -1 * var(--ck-widget-outline-thickness) ); + } + /* * Styles for the horizontal "fake caret" when it should be displayed before the widget (backward keyboard navigation). */ &.ck-widget_type-around_show-fake-caret_before > .ck-widget__type-around > .ck-widget__type-around__fake-caret { - top: -1px; + top: calc( -1 * var(--ck-widget-outline-thickness) - 1px ); display: block; } @@ -76,7 +86,7 @@ * Styles for the horizontal "fake caret" when it should be displayed after the widget (forward keyboard navigation). */ &.ck-widget_type-around_show-fake-caret_after > .ck-widget__type-around > .ck-widget__type-around__fake-caret { - bottom: -1px; + bottom: calc( -1 * var(--ck-widget-outline-thickness) - 1px ); display: block; } } From 9717d216624a82b6dcd3077858c2d2ddfa44a2bb Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 10 Jun 2020 13:22:08 +0200 Subject: [PATCH 67/69] Internal: Added a missing ckeditor5-image dependency to package.json. --- packages/ckeditor5-widget/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ckeditor5-widget/package.json b/packages/ckeditor5-widget/package.json index 64fa1d01784..f0a5e3603d9 100644 --- a/packages/ckeditor5-widget/package.json +++ b/packages/ckeditor5-widget/package.json @@ -26,6 +26,7 @@ "@ckeditor/ckeditor5-essentials": "^19.0.1", "@ckeditor/ckeditor5-heading": "^19.0.1", "@ckeditor/ckeditor5-horizontal-line": "^19.0.1", + "@ckeditor/ckeditor5-image": "^19.0.1", "@ckeditor/ckeditor5-media-embed": "^19.1.0", "@ckeditor/ckeditor5-paragraph": "^19.1.0", "@ckeditor/ckeditor5-table": "^19.1.0", From 73c22728aad5ba6fc4eba0ec7f789d58c198b248 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 10 Jun 2020 15:16:25 +0200 Subject: [PATCH 68/69] Code refactoring: Split the Widget keydown listener into two separate listeners for more flexibility when integrating with other features (TableKeyboard, WidgetTypeAround). --- packages/ckeditor5-table/src/tablekeyboard.js | 36 +---- packages/ckeditor5-widget/src/widget.js | 145 +++++++++++------- 2 files changed, 94 insertions(+), 87 deletions(-) diff --git a/packages/ckeditor5-table/src/tablekeyboard.js b/packages/ckeditor5-table/src/tablekeyboard.js index 226f77be445..fffbc6043db 100644 --- a/packages/ckeditor5-table/src/tablekeyboard.js +++ b/packages/ckeditor5-table/src/tablekeyboard.js @@ -57,7 +57,7 @@ export default class TableKeyboard extends Plugin { // (like Widgets), which take over the keydown events with the "high" priority. Table navigation takes precedence // over Widgets in that matter (widget arrow handler stops propagation of event if object element was selected // but getNearestSelectionRange didn't returned any range). - this.listenTo( viewDocument, 'keydown', ( ...args ) => this._onKeydown( ...args ), { priority: priorities.get( 'high' ) + 1 } ); + this.listenTo( viewDocument, 'keydown', ( ...args ) => this._onKeydown( ...args ), { priority: priorities.get( 'high' ) - 10 } ); } /** @@ -230,19 +230,6 @@ export default class TableKeyboard extends Plugin { return true; } - // If this is an object selected and it's not at the start or the end of cell content - // then let's allow widget handler to take care of it. - const objectElement = selection.getSelectedElement(); - - if ( objectElement && model.schema.isObject( objectElement ) ) { - return false; - } - - // If next to the selection there is an object then this is not the cell boundary (widget handler should handle this). - if ( this._isObjectElementNextToSelection( selection, isForward ) ) { - return false; - } - // If there isn't any $text position between cell edge and selection then we shall move the selection to next cell. const textRange = this._findTextRangeFromSelection( cellRange, selection, isForward ); @@ -307,27 +294,6 @@ export default class TableKeyboard extends Plugin { return focus.isEqual( probe.focus ); } - /** - * Checks if there is an {@link module:engine/model/element~Element element} next to the current - * {@link module:engine/model/selection~Selection model selection} marked in the - * {@link module:engine/model/schema~Schema schema} as an `object`. - * - * @private - * @param {module:engine/model/selection~Selection} modelSelection The selection. - * @param {Boolean} isForward The direction of checking. - * @returns {Boolean} - */ - _isObjectElementNextToSelection( modelSelection, isForward ) { - const model = this.editor.model; - const schema = model.schema; - - const probe = model.createSelection( modelSelection ); - model.modifySelection( probe, { direction: isForward ? 'forward' : 'backward' } ); - const objectElement = isForward ? probe.focus.nodeBefore : probe.focus.nodeAfter; - - return objectElement && schema.isObject( objectElement ); - } - /** * Truncates the range so that it spans from the last selection position * to the last allowed `$text` position (mirrored if `isForward` is false). diff --git a/packages/ckeditor5-widget/src/widget.js b/packages/ckeditor5-widget/src/widget.js index 82ef55f04b6..ed417188f41 100644 --- a/packages/ckeditor5-widget/src/widget.js +++ b/packages/ckeditor5-widget/src/widget.js @@ -18,6 +18,7 @@ import { import env from '@ckeditor/ckeditor5-utils/src/env'; import '../theme/widget.css'; +import priorities from '@ckeditor/ckeditor5-utils/src/priorities'; /** * The widget plugin. It enables base support for widgets. @@ -99,8 +100,24 @@ export default class Widget extends Plugin { view.addObserver( MouseObserver ); this.listenTo( viewDocument, 'mousedown', ( ...args ) => this._onMousedown( ...args ) ); - // Handle custom keydown behaviour. - this.listenTo( viewDocument, 'keydown', ( ...args ) => this._onKeydown( ...args ), { priority: 'high' } ); + // There are two keydown listeners working on different priorities. This allows other + // features such as WidgetTypeAround or TableKeyboard to attach their listeners in between + // and customize the behavior even further in different content/selection scenarios. + // + // * The first listener handles changing the selection on arrow key press + // if the widget is selected or if the selection is next to a widget and the widget + // should become selected upon the arrow key press. + // + // * The second (late) listener makes sure the default browser action on arrow key press is + // prevented when a widget is selected. This prevents the selection from being moved + // from a fake selection container. + this.listenTo( viewDocument, 'keydown', ( ...args ) => { + this._handleSelectionChangeOnArrowKeyPress( ...args ); + }, { priority: 'high' } ); + + this.listenTo( viewDocument, 'keydown', ( ...args ) => { + this._preventDefaultOnArrowKeyPress( ...args ); + }, { priority: priorities.get( 'high' ) - 20 } ); // Handle custom delete behaviour. this.listenTo( viewDocument, 'delete', ( evt, data ) => { @@ -165,25 +182,92 @@ export default class Widget extends Plugin { } /** - * Handles {@link module:engine/view/document~Document#event:keydown keydown} events. + * Handles {@link module:engine/view/document~Document#event:keydown keydown} events and changes + * the model selection when: + * + * * arrow key is pressed when the widget is selected, + * * the selection is next to a widget and the widget should become selected upon the arrow key press. + * + * See {@link #_preventDefaultOnArrowKeyPress}. * * @private * @param {module:utils/eventinfo~EventInfo} eventInfo * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData */ - _onKeydown( eventInfo, domEventData ) { + _handleSelectionChangeOnArrowKeyPress( eventInfo, domEventData ) { const keyCode = domEventData.keyCode; - let wasHandled = false; // Checks if the keys were handled and then prevents the default event behaviour and stops // the propagation. - if ( isArrowKeyCode( keyCode ) ) { - const isForward = isForwardArrowKeyCode( keyCode, this.editor.locale.contentLanguageDirection ); + if ( !isArrowKeyCode( keyCode ) ) { + return; + } + + const model = this.editor.model; + const schema = model.schema; + const modelSelection = model.document.selection; + const objectElement = modelSelection.getSelectedElement(); + const isForward = isForwardArrowKeyCode( keyCode, this.editor.locale.contentLanguageDirection ); + + // If object element is selected. + if ( objectElement && schema.isObject( objectElement ) ) { + const position = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition(); + const newRange = schema.getNearestSelectionRange( position, isForward ? 'forward' : 'backward' ); + + if ( newRange ) { + model.change( writer => { + writer.setSelection( newRange ); + } ); + + domEventData.preventDefault(); + eventInfo.stop(); + + return; + } + } + + // If selection is next to object element. + // Return if not collapsed. + if ( !modelSelection.isCollapsed ) { + return; + } + + const objectElement2 = this._getObjectElementNextToSelection( isForward ); + + if ( !!objectElement2 && schema.isObject( objectElement2 ) ) { + this._setSelectionOverElement( objectElement2 ); + + domEventData.preventDefault(); + eventInfo.stop(); + } + } + + /** + * Handles {@link module:engine/view/document~Document#event:keydown keydown} events and prevents + * the default browser behavior to make sure the fake selection is not being moved from a fake selection + * container. + * + * See {@link #_handleSelectionChangeOnArrowKeyPress}. + * + * @private + * @param {module:utils/eventinfo~EventInfo} eventInfo + * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData + */ + _preventDefaultOnArrowKeyPress( eventInfo, domEventData ) { + const keyCode = domEventData.keyCode; - wasHandled = this._handleArrowKeys( isForward ); + // Checks if the keys were handled and then prevents the default event behaviour and stops + // the propagation. + if ( !isArrowKeyCode( keyCode ) ) { + return; } - if ( wasHandled ) { + const model = this.editor.model; + const schema = model.schema; + const objectElement = model.document.selection.getSelectedElement(); + + // If object element is selected. + if ( objectElement && schema.isObject( objectElement ) ) { domEventData.preventDefault(); eventInfo.stop(); } @@ -231,49 +315,6 @@ export default class Widget extends Plugin { } } - /** - * Handles arrow keys. - * - * @private - * @param {Boolean} isForward Set to true if arrow key should be handled in forward direction. - * @returns {Boolean|undefined} Returns `true` if keys were handled correctly. - */ - _handleArrowKeys( isForward ) { - const model = this.editor.model; - const schema = model.schema; - const modelDocument = model.document; - const modelSelection = modelDocument.selection; - const objectElement = modelSelection.getSelectedElement(); - - // If object element is selected. - if ( objectElement && schema.isObject( objectElement ) ) { - const position = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition(); - const newRange = schema.getNearestSelectionRange( position, isForward ? 'forward' : 'backward' ); - - if ( newRange ) { - model.change( writer => { - writer.setSelection( newRange ); - } ); - } - - return true; - } - - // If selection is next to object element. - // Return if not collapsed. - if ( !modelSelection.isCollapsed ) { - return; - } - - const objectElement2 = this._getObjectElementNextToSelection( isForward ); - - if ( !!objectElement2 && schema.isObject( objectElement2 ) ) { - this._setSelectionOverElement( objectElement2 ); - - return true; - } - } - /** * Sets {@link module:engine/model/selection~Selection document's selection} over given element. * From d5a86cf762d3294cd90e2657cf04ab988765cdd1 Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Wed, 10 Jun 2020 16:13:28 +0200 Subject: [PATCH 69/69] Refactoring in the logic responsible for deleting content around widgets. --- packages/ckeditor5-table/src/tablekeyboard.js | 7 +- packages/ckeditor5-widget/src/widget.js | 10 +- .../src/widgettypearound/widgettypearound.js | 114 +++++++----------- .../widgettypearound/widgettypearound.js | 30 ++++- 4 files changed, 78 insertions(+), 83 deletions(-) diff --git a/packages/ckeditor5-table/src/tablekeyboard.js b/packages/ckeditor5-table/src/tablekeyboard.js index fffbc6043db..6fdf2680228 100644 --- a/packages/ckeditor5-table/src/tablekeyboard.js +++ b/packages/ckeditor5-table/src/tablekeyboard.js @@ -53,10 +53,9 @@ export default class TableKeyboard extends Plugin { this.editor.keystrokes.set( 'Tab', this._getTabHandler( true ), { priority: 'low' } ); this.editor.keystrokes.set( 'Shift+Tab', this._getTabHandler( false ), { priority: 'low' } ); - // Note: This listener has the "high+1" priority because we would like to avoid collisions with other features - // (like Widgets), which take over the keydown events with the "high" priority. Table navigation takes precedence - // over Widgets in that matter (widget arrow handler stops propagation of event if object element was selected - // but getNearestSelectionRange didn't returned any range). + // Note: This listener has the "high-10" priority because it should allow the Widget plugin to handle the default + // behavior first ("high") but it should not be "prevent–defaulted" by the Widget plugin ("high-20") because of + // the fake selection retention on the fully selected widget. this.listenTo( viewDocument, 'keydown', ( ...args ) => this._onKeydown( ...args ), { priority: priorities.get( 'high' ) - 10 } ); } diff --git a/packages/ckeditor5-widget/src/widget.js b/packages/ckeditor5-widget/src/widget.js index ed417188f41..da035fefd04 100644 --- a/packages/ckeditor5-widget/src/widget.js +++ b/packages/ckeditor5-widget/src/widget.js @@ -221,9 +221,9 @@ export default class Widget extends Plugin { domEventData.preventDefault(); eventInfo.stop(); - - return; } + + return; } // If selection is next to object element. @@ -232,10 +232,10 @@ export default class Widget extends Plugin { return; } - const objectElement2 = this._getObjectElementNextToSelection( isForward ); + const objectElementNextToSelection = this._getObjectElementNextToSelection( isForward ); - if ( !!objectElement2 && schema.isObject( objectElement2 ) ) { - this._setSelectionOverElement( objectElement2 ); + if ( objectElementNextToSelection && schema.isObject( objectElementNextToSelection ) ) { + this._setSelectionOverElement( objectElementNextToSelection ); domEventData.preventDefault(); eventInfo.stop(); diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 5d2a4dcc34f..0681781963f 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -67,14 +67,14 @@ export default class WidgetTypeAround extends Plugin { super( editor ); /** - * A reference to the editing model widget element that has the "fake caret" active + * A reference to the model widget element that has the "fake caret" active * on either side of it. It is later used to remove CSS classes associated with the "fake caret" - * when the widget no longer it. + * when the widget no longer needs it. * * @private * @member {module:engine/model/element~Element|null} */ - this._currentFakeCaretModelWidget = null; + this._currentFakeCaretModelElement = null; } /** @@ -93,7 +93,7 @@ export default class WidgetTypeAround extends Plugin { * @inheritDoc */ destroy() { - this._currentFakeCaretModelWidget = null; + this._currentFakeCaretModelElement = null; } /** @@ -218,7 +218,7 @@ export default class WidgetTypeAround extends Plugin { // This is the main listener responsible for the "fake caret". // Note: The priority must precede the default Widget class keydown handler ("high") and the - // TableKeyboard keydown handler ("high + 1"). + // TableKeyboard keydown handler ("high-10"). editingView.document.on( 'keydown', ( evt, domEventData ) => { if ( isArrowKeyCode( domEventData.keyCode ) ) { this._handleArrowKeyPress( evt, domEventData ); @@ -254,14 +254,14 @@ export default class WidgetTypeAround extends Plugin { editor.editing.downcastDispatcher.on( 'selection', ( evt, data, conversionApi ) => { const writer = conversionApi.writer; - if ( this._currentFakeCaretModelWidget ) { - const selectedViewElement = conversionApi.mapper.toViewElement( this._currentFakeCaretModelWidget ); + if ( this._currentFakeCaretModelElement ) { + const selectedViewElement = conversionApi.mapper.toViewElement( this._currentFakeCaretModelElement ); if ( selectedViewElement ) { // Get rid of CSS classes associated with the active ("fake horizontal caret") mode from the view widget. writer.removeClass( POSSIBLE_INSERTION_POSITIONS.map( positionToWidgetCssClass ), selectedViewElement ); - this._currentFakeCaretModelWidget = null; + this._currentFakeCaretModelElement = null; } } @@ -287,7 +287,7 @@ export default class WidgetTypeAround extends Plugin { // Remember the view widget that got the "fake-caret" CSS class. This class should be removed ASAP when the // selection changes - this._currentFakeCaretModelWidget = selectedModelElement; + this._currentFakeCaretModelElement = selectedModelElement; } ); this.listenTo( editor.ui.focusTracker, 'change:isFocused', ( evt, name, isFocused ) => { @@ -582,58 +582,42 @@ export default class WidgetTypeAround extends Plugin { editor.execute( 'delete', { selection: model.createSelection( selectedModelWidget, 'on' ) } ); - } else if ( !isForwardDelete ) { - const range = schema.getNearestSelectionRange( model.createPositionBefore( selectedModelWidget ), direction ); - - if ( range ) { - const deepestEmptyRangeAncestor = getDeepestEmptyPositionAncestor( schema, range.start ); - - // Handle a case when there's an empty document tree branch before the widget that should be deleted. - // - // [] - // - // Note: Range is collapsed, so it does not matter if this is start or end. - if ( deepestEmptyRangeAncestor ) { - model.deleteContent( model.createSelection( deepestEmptyRangeAncestor, 'on' ), { - doNotAutoparagraph: true - } ); - } - // Handle a case when there's a non-empty document tree branch before the widget. - // - // bar[] -> ba[] - // - else { - model.change( writer => { - writer.setSelection( range ); - editor.execute( 'delete' ); - } ); - } - } } else { - const range = schema.getNearestSelectionRange( model.createPositionAfter( selectedModelWidget ), direction ); + const range = schema.getNearestSelectionRange( + model.createPositionAt( selectedModelWidget, typeAroundSelectionAttributeValue ), + direction + ); + // If there is somewhere to move selection to, then there will be something to delete. if ( range ) { - const deepestEmptyRangeAncestor = getDeepestEmptyPositionAncestor( schema, range.start ); - - // Handle a case when there's an empty document tree branch after the widget that should be deleted. - // - // [] - // - // Note: Range is collapsed, so it does not matter if this is start or end. - if ( deepestEmptyRangeAncestor ) { - model.deleteContent( model.createSelection( deepestEmptyRangeAncestor, 'on' ), { - doNotAutoparagraph: true - } ); - } - // Handle a case when there's a non-empty document tree branch after the widget. - // - // []bar -> []ar - // - else { + // If the range is NOT collapsed, then we know that the range contains an object (see getNearestSelectionRange() docs). + if ( !range.isCollapsed ) { model.change( writer => { writer.setSelection( range ); - editor.execute( 'forwardDelete' ); + editor.execute( isForwardDelete ? 'forwardDelete' : 'delete' ); } ); + } else { + const probe = model.createSelection( range.start ); + model.modifySelection( probe, { direction } ); + + // If the range is collapsed, let's see if a non-collapsed range exists that can could be deleted. + // If such range exists, use the editor command because it it safe for collaboration (it merges where it can). + if ( !probe.focus.isEqual( range.start ) ) { + model.change( writer => { + writer.setSelection( range ); + editor.execute( isForwardDelete ? 'forwardDelete' : 'delete' ); + } ); + } + // If there is no non-collapsed range to be deleted then we are sure that there is an empty element + // next to a widget that should be removed. "delete" and "forwardDelete" commands cannot get rid of it + // so calling Model#deleteContent here manually. + else { + const deepestEmptyRangeAncestor = getDeepestEmptyElementAncestor( schema, range.start.parent ); + + model.deleteContent( model.createSelection( deepestEmptyRangeAncestor, 'on' ), { + doNotAutoparagraph: true + } ); + } } } } @@ -709,26 +693,20 @@ function injectFakeCaret( wrapperDomElement ) { wrapperDomElement.appendChild( caretTemplate.render() ); } -// Returns the ancestor of a position closest to the root which is empty. For instance, -// for a position in ``: +// Returns the ancestor of an element closest to the root which is empty. For instance, +// for ``: // -// abc[] +// abc // // it returns ``. // // @param {module:engine/model/schema~Schema} schema -// @param {module:engine/model/position~Position} position +// @param {module:engine/model/element~Element} element // @returns {module:engine/model/element~Element|null} -function getDeepestEmptyPositionAncestor( schema, position ) { - const firstPositionParent = position.parent; - - if ( !firstPositionParent.isEmpty ) { - return null; - } - - let deepestEmptyAncestor = firstPositionParent; +function getDeepestEmptyElementAncestor( schema, element ) { + let deepestEmptyAncestor = element; - for ( const ancestor of firstPositionParent.getAncestors( { parentFirst: true } ) ) { + for ( const ancestor of element.getAncestors( { parentFirst: true } ) ) { if ( ancestor.childCount > 1 || schema.isLimit( ancestor ) ) { break; } diff --git a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js index 73a6919e18a..bf2fd85e9d9 100644 --- a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js @@ -989,6 +989,8 @@ describe( 'WidgetTypeAround', () => { } ); it( 'should delete an empty document tree sub-branch before a widget if the "fake caret" is also before the widget', () => { + let operationType; + setModelData( editor.model, '
' + 'foo' + @@ -1008,14 +1010,21 @@ describe( 'WidgetTypeAround', () => { ); expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + // Assert that the paragraph is merged rather than deleted because + // it is safer for collaboration. + model.on( 'applyOperation', ( evt, [ operation ] ) => { + operationType = operation.type; + } ); + fireDeleteEvent(); expect( getModelData( model ) ).to.equal( '
' + - 'foo' + + 'foo[]' + '
' + - '[]' + '' ); - expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + expect( operationType ).to.equal( 'merge' ); sinon.assert.calledOnce( eventInfoStub.stop ); sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault ); @@ -1142,6 +1151,8 @@ describe( 'WidgetTypeAround', () => { } ); it( 'should delete an empty document tree sub-branch after a widget if the "fake caret" is also after the widget', () => { + let operationType; + setModelData( editor.model, '[]' + '
' + @@ -1161,14 +1172,21 @@ describe( 'WidgetTypeAround', () => { ); expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + // Assert that the paragraph is merged rather than deleted because + // it is safer for collaboration. + model.on( 'applyOperation', ( evt, [ operation ] ) => { + operationType = operation.type; + } ); + fireDeleteEvent( true ); expect( getModelData( model ) ).to.equal( - '[]' + + '' + '
' + - 'foo' + + '[]foo' + '
' ); - expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' ); + expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined; + expect( operationType ).to.equal( 'merge' ); sinon.assert.calledOnce( eventInfoStub.stop ); sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );