diff --git a/package.json b/package.json index 1dee4831..1434eb1b 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "@ckeditor/ckeditor5-engine": "^0.8.0", "@ckeditor/ckeditor5-ui": "^0.7.1", "@ckeditor/ckeditor5-utils": "^0.8.0", - "@ckeditor/ckeditor5-theme-lark": "^0.6.1" + "@ckeditor/ckeditor5-theme-lark": "^0.6.1", + "@ckeditor/ckeditor5-widget": "*" }, "devDependencies": { "@ckeditor/ckeditor5-clipboard": "^0.4.1", diff --git a/src/image.js b/src/image.js index 074fd382..0211b35f 100644 --- a/src/image.js +++ b/src/image.js @@ -9,7 +9,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import ImageEngine from './image/imageengine'; -import Widget from './widget/widget'; +import Widget from '@ckeditor/ckeditor5-widget/src/widget'; import ImageTextAlternative from './imagetextalternative'; import '../theme/theme.scss'; diff --git a/src/image/imageengine.js b/src/image/imageengine.js index 3e5fef44..00c23b2a 100644 --- a/src/image/imageengine.js +++ b/src/image/imageengine.js @@ -9,7 +9,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter'; -import WidgetEngine from '../widget/widgetengine'; +import WidgetEngine from '@ckeditor/ckeditor5-widget/src/widgetengine'; import { viewToModelImage, createImageAttributeConverter } from './converters'; import { toImageWidget } from './utils'; import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement'; diff --git a/src/image/utils.js b/src/image/utils.js index 336ff716..7fccd52c 100644 --- a/src/image/utils.js +++ b/src/image/utils.js @@ -7,7 +7,7 @@ * @module image/image/utils */ -import { toWidget, isWidget } from '../widget/utils'; +import { toWidget, isWidget } from '@ckeditor/ckeditor5-widget/src/utils'; import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element'; const imageSymbol = Symbol( 'isImage' ); @@ -15,7 +15,7 @@ const imageSymbol = Symbol( 'isImage' ); /** * Converts given {@link module:engine/view/element~Element} to image widget: * * adds {@link module:engine/view/element~Element#setCustomProperty custom property} allowing to recognize image widget element, - * * calls {@link module:image/widget/utils~toWidget toWidget} function with proper element's label creator. + * * calls {@link module:widget/utils~toWidget toWidget} function with proper element's label creator. * * @param {module:engine/view/element~Element} viewElement * @param {String} label Element's label. It will be concatenated with image's `alt` attribute if one is present. diff --git a/src/imagecaption/utils.js b/src/imagecaption/utils.js index ff264f4c..a8e54571 100644 --- a/src/imagecaption/utils.js +++ b/src/imagecaption/utils.js @@ -10,7 +10,7 @@ import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element'; import ViewEditableElement from '@ckeditor/ckeditor5-engine/src/view/editableelement'; import { attachPlaceholder } from '@ckeditor/ckeditor5-engine/src/view/placeholder'; -import { toWidgetEditable } from '../widget/utils'; +import { toWidgetEditable } from '@ckeditor/ckeditor5-widget/src/utils'; const captionSymbol = Symbol( 'imageCaption' ); diff --git a/src/widget/utils.js b/src/widget/utils.js deleted file mode 100644 index a83f7bdb..00000000 --- a/src/widget/utils.js +++ /dev/null @@ -1,121 +0,0 @@ -/** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -/** - * @module image/widget/utils - */ - -const widgetSymbol = Symbol( 'isWidget' ); -const labelSymbol = Symbol( 'label' ); - -/** - * CSS class added to each widget element. - * - * @const {String} - */ -export const WIDGET_CLASS_NAME = 'ck-widget'; - -/** - * CSS class added to currently selected widget element. - * - * @const {String} - */ -export const WIDGET_SELECTED_CLASS_NAME = 'ck-widget_selected'; - -/** - * Returns `true` if given {@link module:engine/view/element~Element} is a widget. - * - * @param {module:engine/view/element~Element} element - * @returns {Boolean} - */ -export function isWidget( element ) { - return !!element.getCustomProperty( widgetSymbol ); -} - -/** - * Converts given {@link module:engine/view/element~Element} to widget in following way: - * * sets `contenteditable` attribute to `true`, - * * adds custom `getFillerOffset` method returning `null`, - * * adds `ck-widget` CSS class, - * * adds custom property allowing to recognize widget elements by using {@link ~isWidget}. - * - * @param {module:engine/view/element~Element} element - * @param {Object} [options] - * @param {String|Function} [options.label] Element's label provided to {@link ~setLabel} function. It can be passed as - * a plain string or a function returning a string. - * @returns {module:engine/view/element~Element} Returns same element. - */ -export function toWidget( element, options ) { - options = options || {}; - element.setAttribute( 'contenteditable', false ); - element.getFillerOffset = getFillerOffset; - element.addClass( WIDGET_CLASS_NAME ); - element.setCustomProperty( widgetSymbol, true ); - - if ( options.label ) { - setLabel( element, options.label ); - } - - return element; -} - -/** - * Sets label for given element. - * It can be passed as a plain string or a function returning a string. Function will be called each time label is retrieved by - * {@link ~getLabel}. - * - * @param {module:engine/view/element~Element} element - * @param {String|Function} labelOrCreator - */ -export function setLabel( element, labelOrCreator ) { - element.setCustomProperty( labelSymbol, labelOrCreator ); -} - -/** - * Returns label for provided element. - * - * @param {module:engine/view/element~Element} element - * @return {String} - */ -export function getLabel( element ) { - const labelCreator = element.getCustomProperty( labelSymbol ); - - if ( !labelCreator ) { - return ''; - } - - return typeof labelCreator == 'function' ? labelCreator() : labelCreator; -} - -/** - * Adds functionality to provided {module:engine/view/editableelement~EditableElement} to act as a widget's editable: - * * sets `contenteditable` attribute to `true`, - * * adds `ck-editable` CSS class, - * * adds `ck-editable_focused` CSS class when editable is focused and removes it when it's blurred. - * - * @param {module:engine/view/editableelement~EditableElement} editable - * @returns {module:engine/view/editableelement~EditableElement} Returns same element that was provided in `editable` param. - */ -export function toWidgetEditable( editable ) { - editable.setAttribute( 'contenteditable', 'true' ); - editable.addClass( 'ck-editable' ); - - editable.on( 'change:isFocused', ( evt, property, is ) => { - if ( is ) { - editable.addClass( 'ck-editable_focused' ); - } else { - editable.removeClass( 'ck-editable_focused' ); - } - } ); - - return editable; -} - -// Default filler offset function applied to all widget elements. -// -// @returns {null} -function getFillerOffset() { - return null; -} diff --git a/src/widget/widget.js b/src/widget/widget.js deleted file mode 100644 index 860f5aad..00000000 --- a/src/widget/widget.js +++ /dev/null @@ -1,262 +0,0 @@ -/** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -/** - * @module image/widget/widget - */ - -import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import WidgetEngine from './widgetengine'; -import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobserver'; -import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range'; -import ModelSelection from '@ckeditor/ckeditor5-engine/src/model/selection'; -import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element'; -import ViewEditableElement from '@ckeditor/ckeditor5-engine/src/view/editableelement'; -import RootEditableElement from '@ckeditor/ckeditor5-engine/src/view/rooteditableelement'; -import { isWidget } from './utils'; -import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; - -import '../../theme/widget/theme.scss'; - -/** - * The widget plugin. - * Adds default {@link module:engine/view/document~Document#event:mousedown mousedown} handling on widget elements. - * - * @extends module:core/plugin~Plugin. - */ -export default class Widget extends Plugin { - /** - * @inheritDoc - */ - static get requires() { - return [ WidgetEngine ]; - } - - /** - * @inheritDoc - */ - init() { - const viewDocument = this.editor.editing.view; - - // If mouse down is pressed on widget - create selection over whole widget. - viewDocument.addObserver( MouseObserver ); - this.listenTo( viewDocument, 'mousedown', ( ...args ) => this._onMousedown( ...args ) ); - - // Handle custom keydown behaviour. - this.listenTo( viewDocument, 'keydown', ( ...args ) => this._onKeydown( ...args ), { priority: 'high' } ); - } - - /** - * Handles {@link module:engine/view/document~Document#event:mousedown mousedown} events on widget elements. - * - * @private - * @param {module:utils/eventinfo~EventInfo} eventInfo - * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData - */ - _onMousedown( eventInfo, domEventData ) { - const editor = this.editor; - const viewDocument = editor.editing.view; - let element = domEventData.target; - - // Do nothing if inside nested editable. - if ( isInsideNestedEditable( element ) ) { - return; - } - - // If target is not a widget element - check if one of the ancestors is. - if ( !isWidget( element ) ) { - element = element.findAncestor( isWidget ); - - if ( !element ) { - return; - } - } - - domEventData.preventDefault(); - - // Focus editor if is not focused already. - if ( !viewDocument.isFocused ) { - viewDocument.focus(); - } - - // Create model selection over widget. - const modelElement = editor.editing.mapper.toModelElement( element ); - - editor.document.enqueueChanges( ( ) => { - this._setSelectionOverElement( modelElement ); - } ); - } - - /** - * Handles {@link module:engine/view/document~Document#event:keydown keydown} events. - * - * @private - * @param {module:utils/eventinfo~EventInfo} eventInfo - * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData - */ - _onKeydown( eventInfo, domEventData ) { - const keyCode = domEventData.keyCode; - const isForward = keyCode == keyCodes.delete || keyCode == keyCodes.arrowdown || keyCode == keyCodes.arrowright; - - // Checks if delete/backspace or arrow keys were handled and then prevents default event behaviour and stops - // event propagation. - if ( ( isDeleteKeyCode( keyCode ) && this._handleDelete( isForward ) ) || - ( isArrowKeyCode( keyCode ) && this._handleArrowKeys( isForward ) ) ) { - domEventData.preventDefault(); - eventInfo.stop(); - } - } - - /** - * Handles delete keys: backspace and delete. - * - * @private - * @param {Boolean} isForward Set to true if delete was performed in forward direction. - * @returns {Boolean|undefined} Returns `true` if keys were handled correctly. - */ - _handleDelete( isForward ) { - const modelDocument = this.editor.document; - const modelSelection = modelDocument.selection; - - // Do nothing on non-collapsed selection. - if ( !modelSelection.isCollapsed ) { - return; - } - - const objectElement = this._getObjectElementNextToSelection( isForward ); - - if ( objectElement ) { - modelDocument.enqueueChanges( () => { - // Remove previous element if empty. - const previousNode = modelSelection.anchor.parent; - - if ( previousNode.isEmpty ) { - const batch = modelDocument.batch(); - batch.remove( previousNode ); - } - - this._setSelectionOverElement( objectElement ); - } ); - - return true; - } - } - - /** - * Handles arrow keys. - * - * @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 modelDocument = this.editor.document; - const schema = modelDocument.schema; - const modelSelection = modelDocument.selection; - const objectElement = modelSelection.getSelectedElement(); - - // if object element is selected. - if ( objectElement && schema.objects.has( objectElement.name ) ) { - const position = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition(); - const newRange = modelDocument.getNearestSelectionRange( position, isForward ? 'forward' : 'backward' ); - - if ( newRange ) { - modelDocument.enqueueChanges( () => { - modelSelection.setRanges( [ 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 instanceof ModelElement && modelDocument.schema.objects.has( objectElement2.name ) ) { - modelDocument.enqueueChanges( () => { - this._setSelectionOverElement( objectElement2 ); - } ); - - return true; - } - } - - /** - * Sets {@link module:engine/model/selection~Selection document's selection} over given element. - * - * @private - * @param {module:engine/model/element~Element} element - */ - _setSelectionOverElement( element ) { - this.editor.document.selection.setRanges( [ ModelRange.createOn( element ) ] ); - } - - /** - * Checks if {@link module:engine/model/element~Element element} placed next to the current - * {@link module:engine/model/selection~Selection model selection} exists and is marked in - * {@link module:engine/model/schema~Schema schema} as `object`. - * - * @private - * @param {Boolean} forward Direction of checking. - * @returns {module:engine/model/element~Element|null} - */ - _getObjectElementNextToSelection( forward ) { - const modelDocument = this.editor.document; - const schema = modelDocument.schema; - const modelSelection = modelDocument.selection; - const dataController = this.editor.data; - - // Clone current selection to use it as a probe. We must leave default selection as it is so it can return - // to its current state after undo. - const probe = ModelSelection.createFromSelection( modelSelection ); - dataController.modifySelection( probe, { direction: forward ? 'forward' : 'backward' } ); - const objectElement = forward ? probe.focus.nodeBefore : probe.focus.nodeAfter; - - if ( objectElement instanceof ModelElement && schema.objects.has( objectElement.name ) ) { - return objectElement; - } - - return null; - } -} - -// 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' if provided key code represents one of the delete keys: delete or backspace. -// -//@param {Number} keyCode -//@returns {Boolean} -function isDeleteKeyCode( keyCode ) { - return keyCode == keyCodes.delete || keyCode == keyCodes.backspace; -} - -// Returns `true` when element is a nested editable or is placed inside one. -// -// @param {module:engine/view/element~Element} -// @returns {Boolean} -function isInsideNestedEditable( element ) { - while ( element ) { - if ( element instanceof ViewEditableElement && !( element instanceof RootEditableElement ) ) { - return true; - } - - element = element.parent; - } - - return false; -} diff --git a/src/widget/widgetengine.js b/src/widget/widgetengine.js deleted file mode 100644 index 39915adc..00000000 --- a/src/widget/widgetengine.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -/** - * @module image/widget/widgetengine - */ - -import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import { WIDGET_SELECTED_CLASS_NAME, isWidget, getLabel } from './utils'; - -/** - * The widget engine plugin. - * Registers model to view selection converter for editing pipeline. It is hooked after default selection conversion. - * If converted selection is placed around widget element, selection is marked as fake. Additionally, proper CSS class - * is added to indicate that widget has been selected. - * - * @extends module:core/plugin~Plugin. - */ -export default class WidgetEngine extends Plugin { - /** - * @inheritDoc - */ - init() { - let previouslySelected; - - // Model to view selection converter. - // Converts selection placed over widget element to fake selection - this.editor.editing.modelToView.on( 'selection', ( evt, data, consumable, conversionApi ) => { - // Remove selected class from previously selected widget. - if ( previouslySelected && previouslySelected.hasClass( WIDGET_SELECTED_CLASS_NAME ) ) { - previouslySelected.removeClass( WIDGET_SELECTED_CLASS_NAME ); - } - - const viewSelection = conversionApi.viewSelection; - - // Check if widget was clicked or some sub-element. - const selectedElement = viewSelection.getSelectedElement(); - - if ( !selectedElement || !isWidget( selectedElement ) ) { - return; - } - - viewSelection.setFake( true, { label: getLabel( selectedElement ) } ); - selectedElement.addClass( WIDGET_SELECTED_CLASS_NAME ); - previouslySelected = selectedElement; - }, { priority: 'low' } ); - } -} diff --git a/tests/image.js b/tests/image.js index 1cb7105d..b20c1550 100644 --- a/tests/image.js +++ b/tests/image.js @@ -8,7 +8,7 @@ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import Image from '../src/image'; import ImageEngine from '../src/image/imageengine'; -import Widget from '../src/widget/widget'; +import Widget from '@ckeditor/ckeditor5-widget/src/widget'; import ImageTextAlternative from '../src/imagetextalternative'; describe( 'Image', () => { diff --git a/tests/image/utils.js b/tests/image/utils.js index 230fd1a6..e2c4f4ef 100644 --- a/tests/image/utils.js +++ b/tests/image/utils.js @@ -6,7 +6,7 @@ import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element'; import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element'; import { toImageWidget, isImageWidget, isImage } from '../../src/image/utils'; -import { isWidget, getLabel } from '../../src/widget/utils'; +import { isWidget, getLabel } from '@ckeditor/ckeditor5-widget/src/utils'; describe( 'image widget utils', () => { let element, image; diff --git a/tests/widget/utils.js b/tests/widget/utils.js deleted file mode 100644 index 61f81799..00000000 --- a/tests/widget/utils.js +++ /dev/null @@ -1,116 +0,0 @@ -/** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element'; -import ViewEditableElement from '@ckeditor/ckeditor5-engine/src/view/editableelement'; -import ViewDocument from '@ckeditor/ckeditor5-engine/src/view/document'; -import { - toWidget, - isWidget, - setLabel, - getLabel, - toWidgetEditable, - WIDGET_CLASS_NAME -} from '../../src/widget/utils'; - -describe( 'widget utils', () => { - let element; - - beforeEach( () => { - element = new ViewElement( 'div' ); - toWidget( element ); - } ); - - describe( 'toWidget()', () => { - it( 'should set contenteditable to false', () => { - expect( element.getAttribute( 'contenteditable' ) ).to.be.false; - } ); - - it( 'should define getFillerOffset method', () => { - expect( element.getFillerOffset ).to.be.function; - expect( element.getFillerOffset() ).to.be.null; - } ); - - it( 'should add proper CSS class', () => { - expect( element.hasClass( WIDGET_CLASS_NAME ) ).to.be.true; - } ); - - it( 'should add element\'s label if one is provided', () => { - element = new ViewElement( 'div' ); - toWidget( element, { label: 'foo bar baz label' } ); - - expect( getLabel( element ) ).to.equal( 'foo bar baz label' ); - } ); - - it( 'should add element\'s label if one is provided as function', () => { - element = new ViewElement( 'div' ); - toWidget( element, { label: () => 'foo bar baz label' } ); - - expect( getLabel( element ) ).to.equal( 'foo bar baz label' ); - } ); - } ); - - describe( 'isWidget()', () => { - it( 'should return true for widgetized elements', () => { - expect( isWidget( element ) ).to.be.true; - } ); - - it( 'should return false for non-widgetized elements', () => { - expect( isWidget( new ViewElement( 'p' ) ) ).to.be.false; - } ); - } ); - - describe( 'label utils', () => { - it( 'should allow to set label for element', () => { - const element = new ViewElement( 'p' ); - setLabel( element, 'foo bar baz' ); - - expect( getLabel( element ) ).to.equal( 'foo bar baz' ); - } ); - - it( 'should return empty string for elements without label', () => { - const element = new ViewElement( 'div' ); - - expect( getLabel( element ) ).to.equal( '' ); - } ); - - it( 'should allow to use a function as label creator', () => { - const element = new ViewElement( 'p' ); - let caption = 'foo'; - setLabel( element, () => caption ); - - expect( getLabel( element ) ).to.equal( 'foo' ); - caption = 'bar'; - expect( getLabel( element ) ).to.equal( 'bar' ); - } ); - } ); - - describe( 'toWidgetEditable', () => { - let viewDocument, element; - - beforeEach( () => { - viewDocument = new ViewDocument(); - element = new ViewEditableElement( 'div' ); - element.document = viewDocument; - toWidgetEditable( element ); - } ); - - it( 'should be created in context of proper document', () => { - expect( element.document ).to.equal( viewDocument ); - } ); - - it( 'should add proper class', () => { - expect( element.hasClass( 'ck-editable' ) ).to.be.true; - } ); - - it( 'should add proper class when element is focused', () => { - element.isFocused = true; - expect( element.hasClass( 'ck-editable_focused' ) ).to.be.true; - - element.isFocused = false; - expect( element.hasClass( 'ck-editable_focused' ) ).to.be.false; - } ); - } ); -} ); diff --git a/tests/widget/widget.js b/tests/widget/widget.js deleted file mode 100644 index dc0b057a..00000000 --- a/tests/widget/widget.js +++ /dev/null @@ -1,708 +0,0 @@ -/** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; -import Widget from '../../src/widget/widget'; -import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobserver'; -import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter'; -import { toWidget } from '../../src/widget/utils'; -import ViewContainer from '@ckeditor/ckeditor5-engine/src/view/containerelement'; -import ViewEditable from '@ckeditor/ckeditor5-engine/src/view/editableelement'; -import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata'; -import AttributeContainer from '@ckeditor/ckeditor5-engine/src/view/attributeelement'; -import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; - -/* global document */ - -describe( 'Widget', () => { - let editor, doc, viewDocument; - - beforeEach( () => { - return VirtualTestEditor.create( { - plugins: [ Widget ] - } ) - .then( newEditor => { - editor = newEditor; - doc = editor.document; - viewDocument = editor.editing.view; - - doc.schema.registerItem( 'widget', '$block' ); - doc.schema.objects.add( 'widget' ); - doc.schema.registerItem( 'paragraph', '$block' ); - doc.schema.registerItem( 'inline', '$inline' ); - doc.schema.objects.add( 'inline' ); - doc.schema.registerItem( 'nested' ); - doc.schema.allow( { name: '$inline', inside: 'nested' } ); - doc.schema.allow( { name: 'nested', inside: 'widget' } ); - - buildModelConverter().for( editor.editing.modelToView ) - .fromElement( 'paragraph' ) - .toElement( 'p' ); - - buildModelConverter().for( editor.editing.modelToView ) - .fromElement( 'widget' ) - .toElement( () => { - const b = new AttributeContainer( 'b' ); - const div = new ViewContainer( 'div', null, b ); - - return toWidget( div ); - } ); - - buildModelConverter().for( editor.editing.modelToView ) - .fromElement( 'inline' ) - .toElement( 'figure' ); - - buildModelConverter().for( editor.editing.modelToView ) - .fromElement( 'nested' ) - .toElement( () => new ViewEditable( 'figcaption', { contenteditable: true } ) ); - } ); - } ); - - it( 'should be loaded', () => { - expect( editor.plugins.get( Widget ) ).to.be.instanceOf( Widget ); - } ); - - it( 'should add MouseObserver', () => { - expect( editor.editing.view.getObserver( MouseObserver ) ).to.be.instanceof( MouseObserver ); - } ); - - it( 'should create selection over clicked widget', () => { - setModelData( doc, '[]' ); - const viewDiv = viewDocument.getRoot().getChild( 0 ); - const domEventDataMock = { - target: viewDiv, - preventDefault: sinon.spy() - }; - - viewDocument.fire( 'mousedown', domEventDataMock ); - - expect( getModelData( doc ) ).to.equal( '[]' ); - sinon.assert.calledOnce( domEventDataMock.preventDefault ); - } ); - - it( 'should create selection when clicked in nested element', () => { - setModelData( doc, '[]' ); - const viewDiv = viewDocument.getRoot().getChild( 0 ); - const viewB = viewDiv.getChild( 0 ); - const domEventDataMock = { - target: viewB, - preventDefault: sinon.spy() - }; - - viewDocument.fire( 'mousedown', domEventDataMock ); - - expect( getModelData( doc ) ).to.equal( '[]' ); - sinon.assert.calledOnce( domEventDataMock.preventDefault ); - } ); - - it( 'should do nothing if clicked inside nested editable', () => { - setModelData( doc, '[]foo bar' ); - const viewDiv = viewDocument.getRoot().getChild( 0 ); - const viewFigcaption = viewDiv.getChild( 0 ); - - const domEventDataMock = { - target: viewFigcaption, - preventDefault: sinon.spy() - }; - - viewDocument.fire( 'mousedown', domEventDataMock ); - - sinon.assert.notCalled( domEventDataMock.preventDefault ); - } ); - - it( 'should do nothing if clicked in non-widget element', () => { - setModelData( doc, '[]foo bar' ); - const viewP = viewDocument.getRoot().getChild( 0 ); - const domEventDataMock = { - target: viewP, - preventDefault: sinon.spy() - }; - - viewDocument.focus(); - viewDocument.fire( 'mousedown', domEventDataMock ); - - expect( getModelData( doc ) ).to.equal( '[]foo bar' ); - sinon.assert.notCalled( domEventDataMock.preventDefault ); - } ); - - it( 'should not focus editable if already is focused', () => { - setModelData( doc, '' ); - const widget = viewDocument.getRoot().getChild( 0 ); - const domEventDataMock = { - target: widget, - preventDefault: sinon.spy() - }; - const focusSpy = sinon.spy( viewDocument, 'focus' ); - - viewDocument.isFocused = true; - viewDocument.fire( 'mousedown', domEventDataMock ); - - sinon.assert.calledOnce( domEventDataMock.preventDefault ); - sinon.assert.notCalled( focusSpy ); - expect( getModelData( doc ) ).to.equal( '[]' ); - } ); - - describe( 'keys handling', () => { - describe( 'delete and backspace', () => { - test( - 'should select widget when backspace is pressed', - '[]foo', - keyCodes.backspace, - '[]foo' - ); - - test( - 'should remove empty element after selecting widget when backspace is pressed', - '[]', - keyCodes.backspace, - '[]' - ); - - test( - 'should select widget when delete is pressed', - 'foo[]', - keyCodes.delete, - 'foo[]' - ); - - test( - 'should remove empty element after selecting widget when delete is pressed', - '[]', - keyCodes.delete, - '[]' - ); - - test( - 'should not respond to other keys', - '[]foo', - 65, - '[]foo' - ); - - test( - 'should do nothing on non-collapsed selection', - '[f]oo', - keyCodes.backspace, - '[f]oo' - ); - - test( - 'should do nothing on non-object elements', - 'foo[]bar', - keyCodes.backspace, - 'foo[]bar' - ); - - test( - 'should work correctly with modifier key: backspace + ctrl', - '[]foo', - { keyCode: keyCodes.backspace, ctrlKey: true }, - '[]foo' - ); - - test( - 'should work correctly with modifier key: backspace + alt', - '[]foo', - { keyCode: keyCodes.backspace, altKey: true }, - '[]foo' - ); - - test( - 'should work correctly with modifier key: backspace + shift', - '[]foo', - { keyCode: keyCodes.backspace, shiftKey: true }, - '[]foo' - ); - - test( - 'should work correctly with modifier key: delete + ctrl', - 'foo[]', - { keyCode: keyCodes.delete, ctrlKey: true }, - 'foo[]' - ); - - test( - 'should work correctly with modifier key: delete + alt', - 'foo[]', - { keyCode: keyCodes.delete, altKey: true }, - 'foo[]' - ); - - test( - 'should work correctly with modifier key: delete + shift', - 'foo[]', - { keyCode: keyCodes.delete, shiftKey: true }, - 'foo[]' - ); - - test( - 'should not modify backspace default behaviour in single paragraph boundaries', - '[]foo', - keyCodes.backspace, - '[]foo' - ); - - test( - 'should not modify delete default behaviour in single paragraph boundaries', - 'foo[]', - keyCodes.delete, - 'foo[]' - ); - - test( - 'should do nothing on selected widget preceded by a paragraph - backspace', - 'foo[]', - keyCodes.backspace, - 'foo[]' - ); - - test( - 'should do nothing on selected widget preceded by another widget - backspace', - '[]', - keyCodes.backspace, - '[]' - ); - - test( - 'should do nothing on selected widget before paragraph - backspace', - '[]foo', - keyCodes.backspace, - '[]foo' - ); - - test( - 'should do nothing on selected widget before another widget - backspace', - '[]', - keyCodes.backspace, - '[]' - ); - - test( - 'should do nothing on selected widget between paragraphs - backspace', - 'bar[]foo', - keyCodes.backspace, - 'bar[]foo' - ); - - test( - 'should do nothing on selected widget between other widgets - backspace', - '[]', - keyCodes.backspace, - '[]' - ); - - test( - 'should do nothing on selected widget preceded by a paragraph - delete', - 'foo[]', - keyCodes.delete, - 'foo[]' - ); - - test( - 'should do nothing on selected widget preceded by another widget - delete', - '[]', - keyCodes.delete, - '[]' - ); - - test( - 'should do nothing on selected widget before paragraph - delete', - '[]foo', - keyCodes.delete, - '[]foo' - ); - - test( - 'should do nothing on selected widget before another widget - delete', - '[]', - keyCodes.delete, - '[]' - ); - - test( - 'should do nothing on selected widget between paragraphs - delete', - 'bar[]foo', - keyCodes.delete, - 'bar[]foo' - ); - - test( - 'should do nothing on selected widget between other widgets - delete', - '[]', - keyCodes.delete, - '[]' - ); - - test( - 'should select inline objects - backspace', - 'foo[]bar', - keyCodes.backspace, - 'foo[]bar' - ); - - test( - 'should select inline objects - delete', - 'foo[]bar', - keyCodes.delete, - 'foo[]bar' - ); - - test( - 'should do nothing on selected inline objects - backspace', - 'foo[]bar', - keyCodes.backspace, - 'foo[]bar' - ); - - test( - 'should do nothing on selected inline objects - delete', - 'foo[]bar', - keyCodes.delete, - 'foo[]bar' - ); - - test( - 'should do nothing if selection is placed after first letter - backspace', - 'a[]', - keyCodes.backspace, - 'a[]' - ); - - test( - 'should do nothing if selection is placed before first letter - delete', - '[]a', - keyCodes.delete, - '[]a' - ); - - it( 'should prevent default behaviour and stop event propagation', () => { - const keydownHandler = sinon.spy(); - const domEventDataMock = { - keyCode: keyCodes.delete, - preventDefault: sinon.spy(), - }; - setModelData( doc, 'foo[]' ); - viewDocument.on( 'keydown', keydownHandler ); - - viewDocument.fire( 'keydown', domEventDataMock ); - - expect( getModelData( doc ) ).to.equal( 'foo[]' ); - sinon.assert.calledOnce( domEventDataMock.preventDefault ); - sinon.assert.notCalled( keydownHandler ); - } ); - } ); - - describe( 'arrows', () => { - test( - 'should move selection forward from selected object - right arrow', - '[]foo', - keyCodes.arrowright, - '[]foo' - ); - - test( - 'should move selection forward from selected object - down arrow', - '[]foo', - keyCodes.arrowdown, - '[]foo' - ); - - test( - 'should move selection backward from selected object - left arrow', - 'foo[]', - keyCodes.arrowleft, - 'foo[]' - ); - - test( - 'should move selection backward from selected object - up arrow', - 'foo[]', - keyCodes.arrowup, - 'foo[]' - ); - - test( - 'should move selection to next widget - right arrow', - '[]', - keyCodes.arrowright, - '[]' - ); - - test( - 'should move selection to next widget - down arrow', - '[]', - keyCodes.arrowdown, - '[]' - ); - - test( - 'should move selection to previous widget - left arrow', - '[]', - keyCodes.arrowleft, - '[]' - ); - - test( - 'should move selection to previous widget - up arrow', - '[]', - keyCodes.arrowup, - '[]' - ); - - test( - 'should do nothing on non-collapsed selection next to object - right arrow', - 'ba[r]', - keyCodes.arrowright, - 'ba[r]' - ); - - test( - 'should do nothing on non-collapsed selection next to object - down arrow', - 'ba[r]', - keyCodes.arrowdown, - 'ba[r]' - ); - - test( - 'should do nothing on non-collapsed selection next to object - left arrow', - '[b]ar', - keyCodes.arrowleft, - '[b]ar' - ); - - test( - 'should do nothing on non-collapsed selection next to object - up arrow', - '[b]ar', - keyCodes.arrowup, - '[b]ar' - ); - - test( - 'should not move selection if there is no correct location - right arrow', - 'foo[]', - keyCodes.arrowright, - 'foo[]' - ); - - test( - 'should not move selection if there is no correct location - down arrow', - 'foo[]', - keyCodes.arrowdown, - 'foo[]' - ); - - test( - 'should not move selection if there is no correct location - left arrow', - '[]foo', - keyCodes.arrowleft, - '[]foo' - ); - - test( - 'should not move selection if there is no correct location - up arrow', - '[]foo', - keyCodes.arrowup, - '[]foo' - ); - - it( 'should prevent default behaviour when there is no correct location - document end', () => { - const keydownHandler = sinon.spy(); - const domEventDataMock = { - keyCode: keyCodes.arrowright, - preventDefault: sinon.spy(), - }; - setModelData( doc, 'foo[]' ); - viewDocument.on( 'keydown', keydownHandler ); - - viewDocument.fire( 'keydown', domEventDataMock ); - - expect( getModelData( doc ) ).to.equal( 'foo[]' ); - sinon.assert.calledOnce( domEventDataMock.preventDefault ); - sinon.assert.notCalled( keydownHandler ); - } ); - - it( 'should prevent default behaviour when there is no correct location - document start', () => { - const keydownHandler = sinon.spy(); - const domEventDataMock = { - keyCode: keyCodes.arrowleft, - preventDefault: sinon.spy(), - }; - setModelData( doc, '[]foo' ); - viewDocument.on( 'keydown', keydownHandler ); - - viewDocument.fire( 'keydown', domEventDataMock ); - - expect( getModelData( doc ) ).to.equal( '[]foo' ); - sinon.assert.calledOnce( domEventDataMock.preventDefault ); - sinon.assert.notCalled( keydownHandler ); - } ); - - test( - 'should move selection to object element - right arrow', - 'foo[]', - keyCodes.arrowright, - 'foo[]' - ); - - test( - 'should move selection to object element - down arrow', - 'foo[]', - keyCodes.arrowdown, - 'foo[]' - ); - - test( - 'should move selection to object element - left arrow', - '[]foo', - keyCodes.arrowleft, - '[]foo' - ); - - test( - 'should move selection to object element - up arrow', - '[]foo', - keyCodes.arrowup, - '[]foo' - ); - - test( - 'do nothing on non objects - right arrow', - 'foo[]bar', - keyCodes.arrowright, - 'foo[]bar' - ); - - test( - 'do nothing on non objects - down arrow', - 'foo[]bar', - keyCodes.arrowdown, - 'foo[]bar' - ); - - test( - 'do nothing on non objects - left arrow', - 'foo[]bar', - keyCodes.arrowleft, - 'foo[]bar' - ); - - test( - 'do nothing on non objects - up arrow', - 'foo[]bar', - keyCodes.arrowup, - 'foo[]bar' - ); - - test( - 'should work correctly with modifier key: right arrow + ctrl', - '[]foo', - { keyCode: keyCodes.arrowright, ctrlKey: true }, - '[]foo' - ); - - test( - 'should work correctly with modifier key: right arrow + alt', - '[]foo', - { keyCode: keyCodes.arrowright, altKey: true }, - '[]foo' - ); - - test( - 'should work correctly with modifier key: right arrow + shift', - '[]foo', - { keyCode: keyCodes.arrowright, shiftKey: true }, - '[]foo' - ); - - test( - 'should work correctly with modifier key: down arrow + ctrl', - '[]foo', - { keyCode: keyCodes.arrowdown, ctrlKey: true }, - '[]foo' - ); - - test( - 'should work correctly with modifier key: down arrow + alt', - '[]foo', - { keyCode: keyCodes.arrowdown, altKey: true }, - '[]foo' - ); - - test( - 'should work correctly with modifier key: down arrow + shift', - '[]foo', - { keyCode: keyCodes.arrowdown, shiftKey: true }, - '[]foo' - ); - - test( - 'should work correctly with modifier key: left arrow + ctrl', - 'foo[]', - { keyCode: keyCodes.arrowleft, ctrlKey: true }, - 'foo[]' - ); - - test( - 'should work correctly with modifier key: left arrow + alt', - 'foo[]', - { keyCode: keyCodes.arrowleft, altKey: true }, - 'foo[]' - ); - - test( - 'should work correctly with modifier key: left arrow + shift', - 'foo[]', - { keyCode: keyCodes.arrowleft, shiftKey: true }, - 'foo[]' - ); - - test( - 'should work correctly with modifier key: up arrow + ctrl', - 'foo[]', - { keyCode: keyCodes.arrowup, ctrlKey: true }, - 'foo[]' - ); - - test( - 'should work correctly with modifier key: up arrow + alt', - 'foo[]', - { keyCode: keyCodes.arrowup, altKey: true }, - 'foo[]' - ); - - test( - 'should work correctly with modifier key: up arrow + shift', - 'foo[]', - { keyCode: keyCodes.arrowup, shiftKey: true }, - 'foo[]' - ); - - test( - 'should do nothing if there is more than one selection in model', - '[foo][bar]', - keyCodes.arrowright, - '[foo][bar]' - ); - } ); - - function test( name, data, keyCodeOrMock, expected ) { - it( name, () => { - const domEventDataMock = ( typeof keyCodeOrMock == 'object' ) ? keyCodeOrMock : { - keyCode: keyCodeOrMock - }; - - setModelData( doc, data ); - viewDocument.fire( 'keydown', new DomEventData( - viewDocument, - { target: document.createElement( 'div' ), preventDefault: () => {} }, - domEventDataMock - ) ); - - expect( getModelData( doc ) ).to.equal( expected ); - } ); - } - } ); -} ); diff --git a/tests/widget/widgetengine.js b/tests/widget/widgetengine.js deleted file mode 100644 index 96227ea3..00000000 --- a/tests/widget/widgetengine.js +++ /dev/null @@ -1,97 +0,0 @@ -/** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; -import WidgetEngine from '../../src/widget/widgetengine'; -import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter'; -import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; -import ViewContainer from '@ckeditor/ckeditor5-engine/src/view/containerelement'; -import ViewEditable from '@ckeditor/ckeditor5-engine/src/view/editableelement'; -import { toWidget } from '../../src/widget/utils'; - -describe( 'WidgetEngine', () => { - let editor, document, viewDocument; - - beforeEach( () => { - return VirtualTestEditor.create( { - plugins: [ WidgetEngine ] - } ) - .then( newEditor => { - editor = newEditor; - document = editor.document; - viewDocument = editor.editing.view; - document.schema.registerItem( 'widget', '$block' ); - document.schema.registerItem( 'editable' ); - document.schema.allow( { name: '$inline', inside: 'editable' } ); - document.schema.allow( { name: 'editable', inside: 'widget' } ); - document.schema.allow( { name: 'editable', inside: '$root' } ); - - buildModelConverter().for( editor.editing.modelToView ) - .fromElement( 'widget' ) - .toElement( () => { - const element = toWidget( new ViewContainer( 'div' ), { label: 'element label' } ); - - return element; - } ); - - buildModelConverter().for( editor.editing.modelToView ) - .fromElement( 'editable' ) - .toElement( () => new ViewEditable( 'figcaption', { contenteditable: true } ) ); - } ); - } ); - - it( 'should be loaded', () => { - expect( editor.plugins.get( WidgetEngine ) ).to.be.instanceOf( WidgetEngine ); - } ); - - it( 'should apply fake view selection if model selection is on widget element', () => { - setModelData( document, '[foo bar]' ); - - expect( getViewData( viewDocument ) ).to.equal( - '[
foo bar
]' - ); - expect( viewDocument.selection.isFake ).to.be.true; - } ); - - it( 'should use element\'s label to set fake selection if one is provided', () => { - setModelData( document, '[foo bar]' ); - - expect( viewDocument.selection.fakeSelectionLabel ).to.equal( 'element label' ); - } ); - - it( 'fake selection should be empty if widget is not selected', () => { - setModelData( document, 'foo bar' ); - - expect( viewDocument.selection.fakeSelectionLabel ).to.equal( '' ); - } ); - - it( 'should toggle selected class', () => { - setModelData( document, '[foo]' ); - - expect( getViewData( viewDocument ) ).to.equal( - '[
foo
]' - ); - - document.enqueueChanges( () => { - document.selection.collapseToStart(); - } ); - - expect( getViewData( viewDocument ) ).to.equal( - '[]
foo
' - ); - } ); - - it( 'should do nothing when selection is placed in other editable', () => { - setModelData( document, 'foo bar[baz]' ); - - expect( getViewData( viewDocument ) ).to.equal( - '
' + - '
foo bar
' + - '
' + - '
{baz}
' - ); - } ); -} ); diff --git a/theme/widget/theme.scss b/theme/widget/theme.scss deleted file mode 100644 index 3492aaac..00000000 --- a/theme/widget/theme.scss +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. -// For licensing, see LICENSE.md or http://ckeditor.com/license - -@import '~@ckeditor/ckeditor5-theme-lark/theme/helpers/_colors.scss'; -@import '~@ckeditor/ckeditor5-theme-lark/theme/helpers/_shadow.scss'; -@import '~@ckeditor/ckeditor5-theme-lark/theme/helpers/_states.scss'; -@import '~@ckeditor/ckeditor5-theme-lark/theme/helpers/_spacing.scss'; - -@include ck-color-add( ( - 'widget-blurred': #ddd, - 'widget-hover': #FFD25C -) ); - -$widget-outline-thickness: 3px; - -.ck-widget { - margin: ck-spacing() 0; - padding: 0; - - &.ck-widget_selected, &.ck-widget_selected:hover { - outline: $widget-outline-thickness solid ck-color( 'focus' ); - } - - .ck-editor__editable.ck-blurred &.ck-widget_selected { - outline: $widget-outline-thickness solid ck-color( 'widget-blurred' ); - } - - &:hover { - outline: $widget-outline-thickness solid ck-color( 'widget-hover' ); - } - - .ck-editable { - // The `:focus` style is applied before `.ck-editable_focused` class is rendered in the view. - // These styles show a different border for a blink of an eye, so `:focus` need to have same styles applied. - &.ck-editable_focused, &:focus { - @include ck-focus-ring( 'outline' ); - @include ck-box-shadow( $ck-inner-shadow ); - background-color: ck-color( 'background' ); - } - } -}