From 8e074bedbd8368307a39fe0458687507426267d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 18 May 2018 09:54:39 +0200 Subject: [PATCH 01/57] Introduced BlockToolbar plugin. --- src/toolbar/block/blocktoolbar.css | 32 ++ src/toolbar/block/blocktoolbar.js | 326 +++++++++++++++++ src/toolbar/block/icons/pilcrow.svg | 1 + src/toolbar/block/view/blockbuttonview.js | 48 +++ tests/toolbar/block/blocktoolbar.js | 375 ++++++++++++++++++++ tests/toolbar/block/view/blockbuttonview.js | 41 +++ theme/icons/pilcrow.svg | 1 + 7 files changed, 824 insertions(+) create mode 100644 src/toolbar/block/blocktoolbar.css create mode 100644 src/toolbar/block/blocktoolbar.js create mode 100644 src/toolbar/block/icons/pilcrow.svg create mode 100644 src/toolbar/block/view/blockbuttonview.js create mode 100644 tests/toolbar/block/blocktoolbar.js create mode 100644 tests/toolbar/block/view/blockbuttonview.js create mode 100644 theme/icons/pilcrow.svg diff --git a/src/toolbar/block/blocktoolbar.css b/src/toolbar/block/blocktoolbar.css new file mode 100644 index 00000000..4b5b157b --- /dev/null +++ b/src/toolbar/block/blocktoolbar.css @@ -0,0 +1,32 @@ +/* + * What you're currently looking at is the source code of a legally protected, proprietary software. + * Letters is licensed under a commercial license and protected by copyright law. Where not otherwise indicated, + * all Letters content is authored by CKSource engineers and consists of CKSource-owned intellectual property. + * + * Copyright (c) 2003-2018, CKSource Frederico Knabben. All rights reserved. + */ + +@import '../ui/styles/helpers/_shadow.css'; + +:root { + --ltrs-color-blocktoolbar-icon: var(--ltrs-color-white); +} + +.ck.ck-button.ltrs-toolbar-block-button { + position: absolute; + border: 0; + font-size: var(--ltrs-font-medium); + background-color: var(--ltrs-color-gray-blue-light); + color: var(--ltrs-color-blocktoolbar-icon); + z-index: var(--ltrs-zindex-air); + transition: 200ms background-color ease-in-out; + + & .ck-icon { + font-size: var(--ltrs-font-size-normal); + } + + &.ck-on { + background-color: var(--ltrs-color-gray-blue-hover); + @mixin ltrs__shadow--inset; + } +} diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js new file mode 100644 index 00000000..9b25ad5e --- /dev/null +++ b/src/toolbar/block/blocktoolbar.js @@ -0,0 +1,326 @@ +/** + * Copyright (c) 2016 - 2017, CKSource - Frederico Knabben. All rights reserved. + */ + +/* global window */ + +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; + +import BlockButtonView from './view/blockbuttonview'; +import BalloonPanelView from '../../panel/balloon/balloonpanelview'; +import ToolbarView from '../toolbarview'; + +import ClickObserver from '@ckeditor/ckeditor5-engine/src/view/observer/clickobserver'; +import ContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement'; +import clickOutsideHandler from '../../bindings/clickoutsidehandler'; + +import { getOptimalPosition } from '@ckeditor/ckeditor5-utils/src/dom/position'; +import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect'; + +import iconPilcrow from '../../../theme/icons/pilcrow.svg'; + +/** + * The block toolbar plugin. + * + * @extends module:core/plugin~Plugin + */ +export default class BlockToolbar extends Plugin { + /** + * @inheritDoc + */ + static get pluginName() { + return 'BlockToolbar'; + } + + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + + editor.editing.view.addObserver( ClickObserver ); + + /** + * Toolbar view. + * + * @type {ToolbarView} + */ + this.toolbarView = new ToolbarView( editor.locale ); + + /** + * Panel view. + * + * @type {BalloonPanelView} + */ + this.panelView = this._createPanelView(); + + /** + * Button view. + * + * @type {ButtonView} + */ + this.buttonView = this._createButtonView(); + + /** + * List of block element names that allow do display toolbar next to it. + * This list will be updated by #afterInit method. + * + * @type {Array} + */ + this._allowedElements = []; + + // Close #panelView on click out of the plugin UI. + clickOutsideHandler( { + emitter: this.panelView, + contextElements: [ this.panelView.element, this.buttonView.element ], + activator: () => this.panelView.isVisible, + callback: () => this._hidePanel() + } ); + + // Hide plugin UI when editor switch to read-only. + this.listenTo( editor, 'change:isReadOnly', ( evt, name, isReadOnly ) => { + if ( isReadOnly ) { + this._disable(); + } else { + this._enable(); + } + } ); + + // Enable as default. + this._enable(); + } + + /** + * Creates toolbar components based on given configuration. + * This needs to be done when all plugins are ready. + * + * @inheritDoc + */ + afterInit() { + const factory = this.editor.ui.componentFactory; + const config = this.editor.config.get( 'blockToolbar' ); + + this.toolbarView.fillFromConfig( config, factory ); + + // Hide panel before executing each button in the panel. + for ( const item of this.toolbarView.items ) { + item.on( 'execute', () => this._hidePanel( true ), { priority: 'high' } ); + } + + this._allowedElements = this._getAllowedElements(); + } + + /** + * Creates panel view. + * + * @private + * @returns {BalloonPanelView} + */ + _createPanelView() { + const editor = this.editor; + const panelView = new BalloonPanelView( editor.locale ); + + panelView.content.add( this.toolbarView ); + editor.ui.view.body.add( panelView ); + editor.ui.focusTracker.add( panelView.element ); + + // Close #panelView on `Esc` press. + this.toolbarView.keystrokes.set( 'Esc', ( evt, cancel ) => { + this._hidePanel( true ); + cancel(); + } ); + + return panelView; + } + + /** + * Creates button view. + * + * @private + * @returns {BlockButtonView} + */ + _createButtonView() { + const editor = this.editor; + const buttonView = new BlockButtonView( editor.locale ); + + buttonView.label = editor.t( 'Edit block' ); + buttonView.icon = iconPilcrow; + buttonView.withText = false; + + // Bind panelView to buttonView. + buttonView.bind( 'isOn' ).to( this.panelView, 'isVisible' ); + buttonView.bind( 'tooltip' ).to( this.panelView, 'isVisible', isVisible => !isVisible ); + + // Toggle panelView on buttonView#execute. + this.listenTo( buttonView, 'execute', () => { + if ( !this.panelView.isVisible ) { + this._showPanel(); + } else { + this._hidePanel( true ); + } + } ); + + editor.ui.view.body.add( buttonView ); + editor.ui.focusTracker.add( buttonView.element ); + + return buttonView; + } + + /** + * Returns list of element names that allow to display block button next to it. + * + * @private + */ + _getAllowedElements() { + const elements = [ 'p', 'li' ]; + + for ( const item of this.editor.config.get( 'heading.options' ) || [] ) { + if ( item.view ) { + elements.push( item.view ); + } + } + + return elements; + } + + /** + * Starts displaying button next to allowed elements. + * + * @private + */ + _enable() { + const editor = this.editor; + const view = editor.editing.view; + const viewDocument = view.document; + let targetElement, targetDomElement; + + // Hides panel on a direct selection change. + this.listenTo( editor.model.document.selection, 'change:range', ( evt, data ) => { + if ( data.directChange ) { + this._hidePanel(); + } + } ); + + this.listenTo( view, 'render', () => { + // Get selection parent container, block button will be attached to this element. + targetElement = getParentContainer( viewDocument.selection.getFirstPosition() ); + + const targetName = targetElement.name; + + // Do not attach block button when target element is not on the white list. + if ( !this._allowedElements.includes( targetName ) ) { + this.buttonView.isVisible = false; + + return; + } + + // Get target DOM node. + targetDomElement = view.domConverter.mapViewToDom( targetElement ); + + // Show block button. + this.buttonView.isVisible = true; + + // Attach block button to target DOM element. + this._attachButtonToElement( targetDomElement ); + + // When panel is opened then refresh it position to be properly aligned with block button. + if ( this.panelView.isVisible ) { + this._showPanel(); + } + }, { priority: 'low' } ); + + // Keep button and panel position on window#resize. + this.listenTo( this.buttonView, 'change:isVisible', ( evt, name, isVisible ) => { + if ( isVisible ) { + this.buttonView.listenTo( window, 'resize', () => this._attachButtonToElement( targetDomElement ) ); + } else { + this.buttonView.stopListening( window, 'resize' ); + + // Hide the panel when the button disappears. + this._hidePanel(); + } + } ); + } + + /** + * Stops displaying block button. + * + * @private + */ + _disable() { + this.stopListening( this.editor.model.document.selection, 'change:range' ); + this.stopListening( this.editor.editing.view, 'render' ); + this.stopListening( this.buttonView, 'change:isVisible' ); + this.buttonView.isVisible = false; + this._hidePanel(); + } + + /** + * Attaches #buttonView to the target block of content. + * + * @protected + * @param {HTMLElement} targetElement Target element. + */ + _attachButtonToElement( targetElement ) { + const contentComputedStyles = window.getComputedStyle( targetElement ); + + const editableRect = new Rect( this.editor.ui.view.editableElement ); + const contentPaddingTop = parseInt( contentComputedStyles.paddingTop ); + const contentLineHeight = parseInt( contentComputedStyles.lineHeight ); + + const position = getOptimalPosition( { + element: this.buttonView.element, + target: targetElement, + positions: [ + ( contentRect, buttonRect ) => { + return { + top: contentRect.top + contentPaddingTop + ( ( contentLineHeight - buttonRect.height ) / 2 ), + left: editableRect.left + }; + } + ] + } ); + + this.buttonView.top = position.top; + this.buttonView.left = position.left; + } + + /** + * Shows toolbar attached to the block button. + * When toolbar is already opened then just repositions it. + * + * @private + */ + _showPanel() { + this.panelView.pin( { + target: this.buttonView.element, + limiter: this.editor.ui.view.element + } ); + } + + /** + * Hides toolbar. + * + * @private + * @param {Boolean} [focusEditable=false] When `true` then editable will be focused after hiding panel. + */ + _hidePanel( focusEditable ) { + this.panelView.isVisible = false; + + if ( focusEditable ) { + this.editor.editing.view.focus(); + } + } +} + +// Because the engine.view.writer.getParentContainer is not exported here is a copy. +// See: https://github.com/ckeditor/ckeditor5-engine/issues/628 +function getParentContainer( position ) { + let parent = position.parent; + + while ( !( parent instanceof ContainerElement ) ) { + parent = parent.parent; + } + + return parent; +} diff --git a/src/toolbar/block/icons/pilcrow.svg b/src/toolbar/block/icons/pilcrow.svg new file mode 100644 index 00000000..359434b7 --- /dev/null +++ b/src/toolbar/block/icons/pilcrow.svg @@ -0,0 +1 @@ + diff --git a/src/toolbar/block/view/blockbuttonview.js b/src/toolbar/block/view/blockbuttonview.js new file mode 100644 index 00000000..636c0da3 --- /dev/null +++ b/src/toolbar/block/view/blockbuttonview.js @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2016 - 2017, CKSource - Frederico Knabben. All rights reserved. + */ + +import ButtonView from '../../../button/buttonview'; +import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit'; + +const toPx = toUnit( 'px' ); + +/** + * The block button view class. + * + * @extends {module:ui/button/buttonview~ButtonView} + */ +export default class BlockButtonView extends ButtonView { + /** + * @inheritDoc + */ + constructor( locale ) { + super( locale ); + + const bind = this.bindTemplate; + + /** + * Top offset. + * + * @member {Number} #top + */ + this.set( 'top', 0 ); + + /** + * Left offset. + * + * @member {Number} #left + */ + this.set( 'left', 0 ); + + this.extendTemplate( { + attributes: { + class: 'ltrs-toolbar-block-button', + style: { + top: bind.to( 'top', val => toPx( val ) ), + left: bind.to( 'left', val => toPx( val ) ), + } + } + } ); + } +} diff --git a/tests/toolbar/block/blocktoolbar.js b/tests/toolbar/block/blocktoolbar.js new file mode 100644 index 00000000..03cda5e5 --- /dev/null +++ b/tests/toolbar/block/blocktoolbar.js @@ -0,0 +1,375 @@ +/** + * Copyright (c) 2016 - 2017, CKSource - Frederico Knabben. All rights reserved. + */ + +/* global document, window, Event */ + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import ClickObserver from '@ckeditor/ckeditor5-engine/src/view/observer/clickobserver'; + +import BlockToolbar from '../../../src/toolbar/block/blocktoolbar'; +import ToolbarView from '../../../src/toolbar/toolbarview'; +import BalloonPanelView from '../../../src/panel/balloon/balloonpanelview'; +import BlockButtonView from './../../../src/toolbar/block/view/blockbuttonview'; + +import Heading from '@ckeditor/ckeditor5-heading/src/heading'; +import HeadingButtonsUI from '@ckeditor/ckeditor5-heading/src/headingbuttonsui'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import ParagraphButtonUI from '@ckeditor/ckeditor5-paragraph/src/paragraphbuttonui'; +import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote'; +import List from '@ckeditor/ckeditor5-list/src/list'; + +import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; + +describe( 'BlockToolbar', () => { + let editor, element, blockToolbar; + + beforeEach( () => { + element = document.createElement( 'div' ); + document.body.appendChild( element ); + + return ClassicTestEditor.create( element, { + plugins: [ BlockToolbar, Heading, HeadingButtonsUI, Paragraph, ParagraphButtonUI, BlockQuote, List ], + blockToolbar: [ 'paragraph', 'heading1', 'heading2', 'blockQuote' ] + } ).then( newEditor => { + editor = newEditor; + blockToolbar = editor.plugins.get( BlockToolbar ); + } ); + } ); + + afterEach( () => { + element.remove(); + return editor.destroy(); + } ); + + it( 'should have pluginName property', () => { + expect( BlockToolbar.pluginName ).to.equal( 'BlockToolbar' ); + } ); + + it( 'should register click observer', () => { + expect( editor.editing.view.getObserver( ClickObserver ) ).to.be.instanceOf( ClickObserver ); + } ); + + it( 'should initialize properly without Heading plugin', () => { + const element = document.createElement( 'div' ); + document.body.appendChild( element ); + + return ClassicTestEditor.create( element, { + plugins: [ BlockToolbar, Paragraph, ParagraphButtonUI, BlockQuote, List ], + blockToolbar: [ 'paragraph', 'blockQuote' ] + } ).then( editor => { + element.remove(); + return editor.destroy(); + } ); + } ); + + describe( 'child views', () => { + describe( 'panelView', () => { + it( 'should create view instance', () => { + expect( blockToolbar.panelView ).to.instanceof( BalloonPanelView ); + } ); + + it( 'should be added to ui.view.body collection', () => { + expect( Array.from( editor.ui.view.body ) ).to.include( blockToolbar.panelView ); + } ); + + it( 'should add panelView to ui.focusTracker', () => { + expect( editor.ui.focusTracker.isFocused ).to.false; + + blockToolbar.panelView.element.dispatchEvent( new Event( 'focus' ) ); + + expect( editor.ui.focusTracker.isFocused ).to.true; + } ); + + it( 'should close panelView after `Esc` press and focus view document', () => { + const spy = sinon.spy( editor.editing.view, 'focus' ); + + blockToolbar.panelView.isVisible = true; + + blockToolbar.toolbarView.keystrokes.press( { + keyCode: keyCodes.esc, + preventDefault: () => {}, + stopPropagation: () => {} + } ); + + expect( blockToolbar.panelView.isVisible ).to.false; + sinon.assert.calledOnce( spy ); + } ); + + it( 'should close panelView on click outside the panel and not focus view document', () => { + const spy = sinon.spy(); + + editor.editing.view.on( 'focus', spy ); + blockToolbar.panelView.isVisible = true; + document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) ); + + expect( blockToolbar.panelView.isVisible ).to.false; + sinon.assert.notCalled( spy ); + } ); + + it( 'should not close panelView on click on panel element', () => { + blockToolbar.panelView.isVisible = true; + blockToolbar.panelView.element.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) ); + + expect( blockToolbar.panelView.isVisible ).to.true; + } ); + } ); + + describe( 'toolbarView', () => { + it( 'should create view instance', () => { + expect( blockToolbar.toolbarView ).to.instanceof( ToolbarView ); + } ); + + it( 'should be added to panelView#content collection', () => { + expect( Array.from( blockToolbar.panelView.content ) ).to.include( blockToolbar.toolbarView ); + } ); + + it( 'should initialize toolbar items based on Editor#blockToolbar config', () => { + expect( Array.from( blockToolbar.toolbarView.items ) ).to.length( 4 ); + } ); + + it( 'should hide panel after clicking on the button from toolbar', () => { + blockToolbar.buttonView.fire( 'execute' ); + + expect( blockToolbar.panelView.isVisible ).to.true; + + blockToolbar.toolbarView.items.get( 0 ).fire( 'execute' ); + + expect( blockToolbar.panelView.isVisible ).to.false; + } ); + } ); + + describe( 'buttonView', () => { + it( 'should create view instance', () => { + expect( blockToolbar.buttonView ).to.instanceof( BlockButtonView ); + } ); + + it( 'should be added to editor ui.view.body collection', () => { + expect( Array.from( editor.ui.view.body ) ).to.include( blockToolbar.buttonView ); + } ); + + it( 'should add buttonView to ui.focusTracker', () => { + expect( editor.ui.focusTracker.isFocused ).to.false; + + blockToolbar.buttonView.element.dispatchEvent( new Event( 'focus' ) ); + + expect( editor.ui.focusTracker.isFocused ).to.true; + } ); + + it( 'should pin panelView to the button on #execute event', () => { + expect( blockToolbar.panelView.isVisible ).to.false; + + const spy = sinon.spy( blockToolbar.panelView, 'pin' ); + + blockToolbar.buttonView.fire( 'execute' ); + + expect( blockToolbar.panelView.isVisible ).to.true; + sinon.assert.calledWith( spy, { + target: blockToolbar.buttonView.element, + limiter: editor.ui.view.element + } ); + } ); + + it( 'should hide panelView and focus editable on #execute event when panel was visible', () => { + blockToolbar.panelView.isVisible = true; + const spy = sinon.spy( editor.editing.view, 'focus' ); + + blockToolbar.buttonView.fire( 'execute' ); + + expect( blockToolbar.panelView.isVisible ).to.false; + sinon.assert.calledOnce( spy ); + } ); + + it( 'should bind #isOn to panelView#isVisible', () => { + blockToolbar.panelView.isVisible = false; + + expect( blockToolbar.buttonView.isOn ).to.false; + + blockToolbar.panelView.isVisible = true; + + expect( blockToolbar.buttonView.isOn ).to.true; + } ); + + it( 'should hide button tooltip when panelView is opened', () => { + blockToolbar.panelView.isVisible = false; + + expect( blockToolbar.buttonView.tooltip ).to.true; + + blockToolbar.panelView.isVisible = true; + + expect( blockToolbar.buttonView.tooltip ).to.false; + } ); + } ); + } ); + + describe( 'allowed elements', () => { + it( 'should display button when selection is placed in a paragraph', () => { + setData( editor.model, 'foo[]bar' ); + + expect( blockToolbar.buttonView.isVisible ).to.true; + } ); + + it( 'should display button when selection is placed in a heading1', () => { + setData( editor.model, 'foo[]bar' ); + + expect( blockToolbar.buttonView.isVisible ).to.true; + } ); + + it( 'should display button when selection is placed in a heading2', () => { + setData( editor.model, 'foo[]bar' ); + + expect( blockToolbar.buttonView.isVisible ).to.true; + } ); + + it( 'should display button when selection is placed in a heading3', () => { + setData( editor.model, 'foo[]bar' ); + + expect( blockToolbar.buttonView.isVisible ).to.true; + } ); + + it( 'should display button when selection is placed in a list item', () => { + setData( editor.model, 'foo[]bar' ); + + expect( blockToolbar.buttonView.isVisible ).to.true; + } ); + + it( 'should display button when selection is placed in a allowed element in a blockQuote', () => { + setData( editor.model, '
foo[]bar
' ); + + expect( blockToolbar.buttonView.isVisible ).to.true; + } ); + + it( 'should not display button when selection is placed in not allowed element', () => { + editor.model.schema.register( 'table', { inheritAllFrom: '$block' } ); + editor.conversion.elementToElement( { model: 'table', view: 'table' } ); + + setData( editor.model, 'foo[]bar
' ); + + expect( blockToolbar.buttonView.isVisible ).to.false; + } ); + } ); + + describe( 'attaching button to the content', () => { + it( 'should attach button to the left side of selected content and center with the first line on view#render', () => { + setData( editor.model, 'foo[]bar' ); + + const target = editor.ui.view.editableElement.querySelector( 'p' ); + + target.style.lineHeight = '20px'; + target.style.paddingTop = '10px'; + + const editableRectSpy = sinon.stub( editor.ui.view.editableElement, 'getBoundingClientRect' ).returns( { + left: 100 + } ); + + const targetRectSpy = sinon.stub( target, 'getBoundingClientRect' ).returns( { + top: 500, + left: 300 + } ); + + const buttonRectSpy = sinon.stub( blockToolbar.buttonView.element, 'getBoundingClientRect' ).returns( { + width: 100, + height: 100 + } ); + + editor.editing.view.fire( 'render' ); + + expect( blockToolbar.buttonView.top ).to.equal( 470 ); + expect( blockToolbar.buttonView.left ).to.equal( 100 ); + + editableRectSpy.restore(); + targetRectSpy.restore(); + buttonRectSpy.restore(); + } ); + + it( 'should reposition panelView when is opened on view#render', () => { + blockToolbar.panelView.isVisible = false; + + const spy = sinon.spy( blockToolbar.panelView, 'pin' ); + + editor.editing.view.fire( 'render' ); + + sinon.assert.notCalled( spy ); + + blockToolbar.panelView.isVisible = true; + + editor.editing.view.fire( 'render' ); + + sinon.assert.calledWith( spy, { + target: blockToolbar.buttonView.element, + limiter: editor.ui.view.element + } ); + } ); + + it( 'should hide opened panel on a selection direct change', () => { + blockToolbar.panelView.isVisible = true; + + editor.model.document.selection.fire( 'change:range', { directChange: true } ); + + expect( blockToolbar.panelView.isVisible ).to.false; + } ); + + it( 'should not hide opened panel on a selection not direct change', () => { + blockToolbar.panelView.isVisible = true; + + editor.model.document.selection.fire( 'change:range', { directChange: false } ); + + expect( blockToolbar.panelView.isVisible ).to.true; + } ); + + it( 'should hide button and stop attaching it when editor switch to readonly', () => { + setData( editor.model, 'foo[]bar' ); + + blockToolbar.panelView.isVisible = true; + + expect( blockToolbar.buttonView.isVisible ).to.true; + expect( blockToolbar.panelView.isVisible ).to.true; + + editor.isReadOnly = true; + + expect( blockToolbar.buttonView.isVisible ).to.false; + expect( blockToolbar.panelView.isVisible ).to.false; + + editor.editing.view.fire( 'render' ); + + expect( blockToolbar.buttonView.isVisible ).to.false; + expect( blockToolbar.panelView.isVisible ).to.false; + + editor.isReadOnly = false; + editor.editing.view.fire( 'render' ); + + expect( blockToolbar.buttonView.isVisible ).to.true; + expect( blockToolbar.panelView.isVisible ).to.false; + } ); + + it( 'should update button position on browser resize only when button is visible', () => { + editor.model.schema.register( 'table', { inheritAllFrom: '$block' } ); + editor.conversion.elementToElement( { model: 'table', view: 'table' } ); + + const spy = sinon.spy( blockToolbar, '_attachButtonToElement' ); + + setData( editor.model, 'fo[]o
bar' ); + + window.dispatchEvent( new Event( 'resize' ) ); + + sinon.assert.notCalled( spy ); + + setData( editor.model, 'foo
ba[]r' ); + + spy.reset(); + + window.dispatchEvent( new Event( 'resize' ) ); + + sinon.assert.called( spy ); + + setData( editor.model, 'fo[]o
bar' ); + + spy.reset(); + + window.dispatchEvent( new Event( 'resize' ) ); + + sinon.assert.notCalled( spy ); + } ); + } ); +} ); diff --git a/tests/toolbar/block/view/blockbuttonview.js b/tests/toolbar/block/view/blockbuttonview.js new file mode 100644 index 00000000..792edd92 --- /dev/null +++ b/tests/toolbar/block/view/blockbuttonview.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2016 - 2017, CKSource - Frederico Knabben. All rights reserved. + */ + +import BlockButtonView from '../../../../src/toolbar/block/view/blockbuttonview'; + +describe( 'BlockButtonView', () => { + let view; + + beforeEach( () => { + view = new BlockButtonView(); + + view.render(); + } ); + + it( 'should create element from template', () => { + expect( view.element.classList.contains( 'ltrs-toolbar-block-button' ) ).to.true; + } ); + + describe( 'DOM binding', () => { + it( 'should react on `view#top` change', () => { + view.top = 0; + + expect( view.element.style.top ).to.equal( '0px' ); + + view.top = 10; + + expect( view.element.style.top ).to.equal( '10px' ); + } ); + + it( 'should react on `view#left` change', () => { + view.left = 0; + + expect( view.element.style.left ).to.equal( '0px' ); + + view.left = 10; + + expect( view.element.style.left ).to.equal( '10px' ); + } ); + } ); +} ); diff --git a/theme/icons/pilcrow.svg b/theme/icons/pilcrow.svg new file mode 100644 index 00000000..359434b7 --- /dev/null +++ b/theme/icons/pilcrow.svg @@ -0,0 +1 @@ + From 85d490fbee975dcf0fa1d47cfa23cac6346fa686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 18 May 2018 10:07:40 +0200 Subject: [PATCH 02/57] Added missing dev dependencies. --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index b44b424b..43444edc 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ }, "devDependencies": { "@ckeditor/ckeditor5-basic-styles": "^10.0.0", + "@ckeditor/ckeditor5-block-quote": "^10.0.0", "@ckeditor/ckeditor5-cloud-services": "^10.0.0", "@ckeditor/ckeditor5-editor-classic": "^10.0.0", "@ckeditor/ckeditor5-engine": "^10.0.0", @@ -23,6 +24,7 @@ "@ckeditor/ckeditor5-heading": "^10.0.0", "@ckeditor/ckeditor5-image": "^10.0.0", "@ckeditor/ckeditor5-link": "^10.0.0", + "@ckeditor/ckeditor5-list": "^10.0.0", "@ckeditor/ckeditor5-paragraph": "^10.0.0", "@ckeditor/ckeditor5-typing": "^10.0.0", "@ckeditor/ckeditor5-undo": "^10.0.0", From 660f62b6bb1f85b40cbbea308b774370d292b11e Mon Sep 17 00:00:00 2001 From: Damian Konopka Date: Fri, 18 May 2018 10:29:07 +0200 Subject: [PATCH 03/57] Moved blocktoolbar styles to correct directory. --- src/toolbar/block/blocktoolbar.css | 32 ----------------------- theme/components/toolbar/blocktoolbar.css | 28 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 32 deletions(-) delete mode 100644 src/toolbar/block/blocktoolbar.css create mode 100644 theme/components/toolbar/blocktoolbar.css diff --git a/src/toolbar/block/blocktoolbar.css b/src/toolbar/block/blocktoolbar.css deleted file mode 100644 index 4b5b157b..00000000 --- a/src/toolbar/block/blocktoolbar.css +++ /dev/null @@ -1,32 +0,0 @@ -/* - * What you're currently looking at is the source code of a legally protected, proprietary software. - * Letters is licensed under a commercial license and protected by copyright law. Where not otherwise indicated, - * all Letters content is authored by CKSource engineers and consists of CKSource-owned intellectual property. - * - * Copyright (c) 2003-2018, CKSource Frederico Knabben. All rights reserved. - */ - -@import '../ui/styles/helpers/_shadow.css'; - -:root { - --ltrs-color-blocktoolbar-icon: var(--ltrs-color-white); -} - -.ck.ck-button.ltrs-toolbar-block-button { - position: absolute; - border: 0; - font-size: var(--ltrs-font-medium); - background-color: var(--ltrs-color-gray-blue-light); - color: var(--ltrs-color-blocktoolbar-icon); - z-index: var(--ltrs-zindex-air); - transition: 200ms background-color ease-in-out; - - & .ck-icon { - font-size: var(--ltrs-font-size-normal); - } - - &.ck-on { - background-color: var(--ltrs-color-gray-blue-hover); - @mixin ltrs__shadow--inset; - } -} diff --git a/theme/components/toolbar/blocktoolbar.css b/theme/components/toolbar/blocktoolbar.css new file mode 100644 index 00000000..27c9000d --- /dev/null +++ b/theme/components/toolbar/blocktoolbar.css @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + + +:root { + /* --ck-color-blocktoolbar-icon: hsla(0, 0%, 100%); */ +} + +.ck.ck-button.ltrs-toolbar-block-button { + position: absolute; + border: 0; + /* font-size: var(--ltrs-font-medium); */ + /* background-color: var(--ltrs-color-gray-blue-light); */ + /* color: var(--ck-color-blocktoolbar-icon); */ + z-index: var(--ck-z-default); + /* transition: 200ms background-color ease-in-out; */ + + & .ck-icon { + font-size: var(--ck-font-size-normal); + } + + /* &.ck-on { + background-color: var(--ltrs-color-gray-blue-hover); + @mixin ltrs__shadow--inset; + } */ +} From 970e3578d0160ffef83a54d9fbbdb1843d33479e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 18 May 2018 10:47:43 +0200 Subject: [PATCH 04/57] Tests: Added very basic block toolbar manual test. --- tests/manual/blocktoolbar/blocktoolbar.html | 12 +++++++ tests/manual/blocktoolbar/blocktoolbar.js | 36 +++++++++++++++++++++ tests/manual/blocktoolbar/blocktoolbar.md | 1 + 3 files changed, 49 insertions(+) create mode 100644 tests/manual/blocktoolbar/blocktoolbar.html create mode 100644 tests/manual/blocktoolbar/blocktoolbar.js create mode 100644 tests/manual/blocktoolbar/blocktoolbar.md diff --git a/tests/manual/blocktoolbar/blocktoolbar.html b/tests/manual/blocktoolbar/blocktoolbar.html new file mode 100644 index 00000000..ce09ffc0 --- /dev/null +++ b/tests/manual/blocktoolbar/blocktoolbar.html @@ -0,0 +1,12 @@ +
+

This is a first line of text.

+

This is a second line of text.

+

This is the end of text.

+
+ + diff --git a/tests/manual/blocktoolbar/blocktoolbar.js b/tests/manual/blocktoolbar/blocktoolbar.js new file mode 100644 index 00000000..80e552f6 --- /dev/null +++ b/tests/manual/blocktoolbar/blocktoolbar.js @@ -0,0 +1,36 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals window, document, console:false */ + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; +import HeadingButtonsUI from '@ckeditor/ckeditor5-heading/src/headingbuttonsui'; +import ParagraphButtonUI from '@ckeditor/ckeditor5-paragraph/src/paragraphbuttonui'; +import BlockToolbar from '../../../src/toolbar/block/blocktoolbar'; +import Range from '@ckeditor/ckeditor5-engine/src/model/range'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ ArticlePluginSet, HeadingButtonsUI, ParagraphButtonUI, BlockToolbar ], + blockToolbar: [ 'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', 'blockQuote' ] + } ) + .then( editor => { + window.editor = editor; + + const balloonToolbar = editor.plugins.get( 'BalloonToolbar' ); + + balloonToolbar.on( 'show', evt => { + const selectionRange = editor.model.document.selection.getFirstRange(); + const blockRange = Range.createOn( editor.model.document.getRoot().getChild( 0 ) ); + + if ( selectionRange.containsRange( blockRange ) || selectionRange.isIntersecting( blockRange ) ) { + evt.stop(); + } + }, { priority: 'high' } ); + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/tests/manual/blocktoolbar/blocktoolbar.md b/tests/manual/blocktoolbar/blocktoolbar.md new file mode 100644 index 00000000..b24ba871 --- /dev/null +++ b/tests/manual/blocktoolbar/blocktoolbar.md @@ -0,0 +1 @@ +## Block toolbar demo From 6bb0a8038b4abcda2664b99dc52aa26f0cb69e68 Mon Sep 17 00:00:00 2001 From: Damian Konopka Date: Fri, 18 May 2018 11:06:15 +0200 Subject: [PATCH 05/57] Changed `ltrs-toolbar-block-button` to `ck-toolbar-block-button`. --- src/toolbar/block/view/blockbuttonview.js | 2 +- tests/toolbar/block/view/blockbuttonview.js | 2 +- theme/components/toolbar/blocktoolbar.css | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/toolbar/block/view/blockbuttonview.js b/src/toolbar/block/view/blockbuttonview.js index 636c0da3..492f93a9 100644 --- a/src/toolbar/block/view/blockbuttonview.js +++ b/src/toolbar/block/view/blockbuttonview.js @@ -37,7 +37,7 @@ export default class BlockButtonView extends ButtonView { this.extendTemplate( { attributes: { - class: 'ltrs-toolbar-block-button', + class: 'ck-toolbar-block-button', style: { top: bind.to( 'top', val => toPx( val ) ), left: bind.to( 'left', val => toPx( val ) ), diff --git a/tests/toolbar/block/view/blockbuttonview.js b/tests/toolbar/block/view/blockbuttonview.js index 792edd92..890ad86a 100644 --- a/tests/toolbar/block/view/blockbuttonview.js +++ b/tests/toolbar/block/view/blockbuttonview.js @@ -14,7 +14,7 @@ describe( 'BlockButtonView', () => { } ); it( 'should create element from template', () => { - expect( view.element.classList.contains( 'ltrs-toolbar-block-button' ) ).to.true; + expect( view.element.classList.contains( 'ck-toolbar-block-button' ) ).to.true; } ); describe( 'DOM binding', () => { diff --git a/theme/components/toolbar/blocktoolbar.css b/theme/components/toolbar/blocktoolbar.css index 27c9000d..12030bc7 100644 --- a/theme/components/toolbar/blocktoolbar.css +++ b/theme/components/toolbar/blocktoolbar.css @@ -8,7 +8,7 @@ /* --ck-color-blocktoolbar-icon: hsla(0, 0%, 100%); */ } -.ck.ck-button.ltrs-toolbar-block-button { +.ck.ck-button.ck-toolbar-block-button { position: absolute; border: 0; /* font-size: var(--ltrs-font-medium); */ From 315c7a7b666512a963aa3e8fbae8d20fa37357c4 Mon Sep 17 00:00:00 2001 From: Damian Konopka Date: Fri, 18 May 2018 11:41:28 +0200 Subject: [PATCH 06/57] Added importing stylesheet to blocktoolbar view. --- src/toolbar/block/view/blockbuttonview.js | 1 + theme/components/toolbar/blocktoolbar.css | 18 +----------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/src/toolbar/block/view/blockbuttonview.js b/src/toolbar/block/view/blockbuttonview.js index 492f93a9..db233191 100644 --- a/src/toolbar/block/view/blockbuttonview.js +++ b/src/toolbar/block/view/blockbuttonview.js @@ -4,6 +4,7 @@ import ButtonView from '../../../button/buttonview'; import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit'; +import '../../../../theme/components/toolbar/blocktoolbar.css'; const toPx = toUnit( 'px' ); diff --git a/theme/components/toolbar/blocktoolbar.css b/theme/components/toolbar/blocktoolbar.css index 12030bc7..0924c5ca 100644 --- a/theme/components/toolbar/blocktoolbar.css +++ b/theme/components/toolbar/blocktoolbar.css @@ -3,26 +3,10 @@ * For licensing, see LICENSE.md. */ - -:root { - /* --ck-color-blocktoolbar-icon: hsla(0, 0%, 100%); */ -} - -.ck.ck-button.ck-toolbar-block-button { +.ck.ck-toolbar-block-button { position: absolute; - border: 0; /* font-size: var(--ltrs-font-medium); */ /* background-color: var(--ltrs-color-gray-blue-light); */ /* color: var(--ck-color-blocktoolbar-icon); */ z-index: var(--ck-z-default); - /* transition: 200ms background-color ease-in-out; */ - - & .ck-icon { - font-size: var(--ck-font-size-normal); - } - - /* &.ck-on { - background-color: var(--ltrs-color-gray-blue-hover); - @mixin ltrs__shadow--inset; - } */ } From 159dee0bb9354daa91bb145844b4e640a1379669 Mon Sep 17 00:00:00 2001 From: Damian Konopka Date: Fri, 18 May 2018 11:53:26 +0200 Subject: [PATCH 07/57] Removed obsolete CSS properties. --- theme/components/toolbar/blocktoolbar.css | 3 --- 1 file changed, 3 deletions(-) diff --git a/theme/components/toolbar/blocktoolbar.css b/theme/components/toolbar/blocktoolbar.css index 0924c5ca..e909f684 100644 --- a/theme/components/toolbar/blocktoolbar.css +++ b/theme/components/toolbar/blocktoolbar.css @@ -5,8 +5,5 @@ .ck.ck-toolbar-block-button { position: absolute; - /* font-size: var(--ltrs-font-medium); */ - /* background-color: var(--ltrs-color-gray-blue-light); */ - /* color: var(--ck-color-blocktoolbar-icon); */ z-index: var(--ck-z-default); } From 113984efa0076ef7d88506e49c5e9aa390beca2e Mon Sep 17 00:00:00 2001 From: Damian Konopka Date: Fri, 18 May 2018 12:19:30 +0200 Subject: [PATCH 08/57] Added extra class to blockToolbar.panelView. --- src/toolbar/block/blocktoolbar.js | 1 + tests/toolbar/block/blocktoolbar.js | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index 9b25ad5e..e1ff6532 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -121,6 +121,7 @@ export default class BlockToolbar extends Plugin { const panelView = new BalloonPanelView( editor.locale ); panelView.content.add( this.toolbarView ); + panelView.className = 'ck-balloon-panel-block-toolbar'; editor.ui.view.body.add( panelView ); editor.ui.focusTracker.add( panelView.element ); diff --git a/tests/toolbar/block/blocktoolbar.js b/tests/toolbar/block/blocktoolbar.js index 03cda5e5..10108979 100644 --- a/tests/toolbar/block/blocktoolbar.js +++ b/tests/toolbar/block/blocktoolbar.js @@ -70,6 +70,10 @@ describe( 'BlockToolbar', () => { expect( blockToolbar.panelView ).to.instanceof( BalloonPanelView ); } ); + it( 'should have additional class name', () => { + expect( blockToolbar.panelView.className ).to.equal( 'ck-balloon-panel-block-toolbar' ); + } ); + it( 'should be added to ui.view.body collection', () => { expect( Array.from( editor.ui.view.body ) ).to.include( blockToolbar.panelView ); } ); From 136e116b6e424cff76d1d0fa873e01400277c2c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 18 May 2018 12:47:09 +0200 Subject: [PATCH 09/57] Fix: Fixed NaN value in a case of missing line-height property. --- src/toolbar/block/blocktoolbar.js | 2 +- tests/toolbar/block/blocktoolbar.js | 34 ++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index e1ff6532..9f657124 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -267,7 +267,7 @@ export default class BlockToolbar extends Plugin { const editableRect = new Rect( this.editor.ui.view.editableElement ); const contentPaddingTop = parseInt( contentComputedStyles.paddingTop ); - const contentLineHeight = parseInt( contentComputedStyles.lineHeight ); + const contentLineHeight = parseInt( contentComputedStyles.lineHeight ) || parseInt( contentComputedStyles.fontSize ); const position = getOptimalPosition( { element: this.buttonView.element, diff --git a/tests/toolbar/block/blocktoolbar.js b/tests/toolbar/block/blocktoolbar.js index 10108979..5228070b 100644 --- a/tests/toolbar/block/blocktoolbar.js +++ b/tests/toolbar/block/blocktoolbar.js @@ -255,7 +255,7 @@ describe( 'BlockToolbar', () => { } ); describe( 'attaching button to the content', () => { - it( 'should attach button to the left side of selected content and center with the first line on view#render', () => { + it( 'should attach button to the left side of selected content and center with the first line on view#render #1', () => { setData( editor.model, 'foo[]bar' ); const target = editor.ui.view.editableElement.querySelector( 'p' ); @@ -287,6 +287,38 @@ describe( 'BlockToolbar', () => { buttonRectSpy.restore(); } ); + it( 'should attach button to the left side of selected content and center with the first line on view#render #2', () => { + setData( editor.model, 'foo[]bar' ); + + const target = editor.ui.view.editableElement.querySelector( 'p' ); + + target.style.fontSize = '20px'; + target.style.paddingTop = '10px'; + + const editableRectSpy = sinon.stub( editor.ui.view.editableElement, 'getBoundingClientRect' ).returns( { + left: 100 + } ); + + const targetRectSpy = sinon.stub( target, 'getBoundingClientRect' ).returns( { + top: 500, + left: 300 + } ); + + const buttonRectSpy = sinon.stub( blockToolbar.buttonView.element, 'getBoundingClientRect' ).returns( { + width: 100, + height: 100 + } ); + + editor.editing.view.fire( 'render' ); + + expect( blockToolbar.buttonView.top ).to.equal( 470 ); + expect( blockToolbar.buttonView.left ).to.equal( 100 ); + + editableRectSpy.restore(); + targetRectSpy.restore(); + buttonRectSpy.restore(); + } ); + it( 'should reposition panelView when is opened on view#render', () => { blockToolbar.panelView.isVisible = false; From 7fb90972976e0772aad9f7f4e9081be10be287c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 18 May 2018 12:47:28 +0200 Subject: [PATCH 10/57] Tests: Removed unused code from MT. --- tests/manual/blocktoolbar/blocktoolbar.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/manual/blocktoolbar/blocktoolbar.js b/tests/manual/blocktoolbar/blocktoolbar.js index 80e552f6..6d61fe7a 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.js +++ b/tests/manual/blocktoolbar/blocktoolbar.js @@ -10,7 +10,6 @@ import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articleplugi import HeadingButtonsUI from '@ckeditor/ckeditor5-heading/src/headingbuttonsui'; import ParagraphButtonUI from '@ckeditor/ckeditor5-paragraph/src/paragraphbuttonui'; import BlockToolbar from '../../../src/toolbar/block/blocktoolbar'; -import Range from '@ckeditor/ckeditor5-engine/src/model/range'; ClassicEditor .create( document.querySelector( '#editor' ), { @@ -19,17 +18,6 @@ ClassicEditor } ) .then( editor => { window.editor = editor; - - const balloonToolbar = editor.plugins.get( 'BalloonToolbar' ); - - balloonToolbar.on( 'show', evt => { - const selectionRange = editor.model.document.selection.getFirstRange(); - const blockRange = Range.createOn( editor.model.document.getRoot().getChild( 0 ) ); - - if ( selectionRange.containsRange( blockRange ) || selectionRange.isIntersecting( blockRange ) ) { - evt.stop(); - } - }, { priority: 'high' } ); } ) .catch( err => { console.error( err.stack ); From e45d69d41003b5cf3b2cbdd7ff5b58bb4045542d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 18 May 2018 12:48:06 +0200 Subject: [PATCH 11/57] Tests: Changed sinon.reset() to sinon.resetHistory(). --- tests/toolbar/block/blocktoolbar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/toolbar/block/blocktoolbar.js b/tests/toolbar/block/blocktoolbar.js index 5228070b..f6c27132 100644 --- a/tests/toolbar/block/blocktoolbar.js +++ b/tests/toolbar/block/blocktoolbar.js @@ -393,7 +393,7 @@ describe( 'BlockToolbar', () => { setData( editor.model, 'foo
ba[]r' ); - spy.reset(); + spy.resetHistory(); window.dispatchEvent( new Event( 'resize' ) ); @@ -401,7 +401,7 @@ describe( 'BlockToolbar', () => { setData( editor.model, 'fo[]o
bar' ); - spy.reset(); + spy.resetHistory(); window.dispatchEvent( new Event( 'resize' ) ); From 808db85eb905942d6a6034797e79588714898f91 Mon Sep 17 00:00:00 2001 From: Damian Konopka Date: Fri, 18 May 2018 13:12:52 +0200 Subject: [PATCH 12/57] Changed naming of block toolbar. --- src/toolbar/block/view/blockbuttonview.js | 2 +- tests/toolbar/block/view/blockbuttonview.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/toolbar/block/view/blockbuttonview.js b/src/toolbar/block/view/blockbuttonview.js index db233191..53c05cbd 100644 --- a/src/toolbar/block/view/blockbuttonview.js +++ b/src/toolbar/block/view/blockbuttonview.js @@ -38,7 +38,7 @@ export default class BlockButtonView extends ButtonView { this.extendTemplate( { attributes: { - class: 'ck-toolbar-block-button', + class: 'ck-block-toolbar-button', style: { top: bind.to( 'top', val => toPx( val ) ), left: bind.to( 'left', val => toPx( val ) ), diff --git a/tests/toolbar/block/view/blockbuttonview.js b/tests/toolbar/block/view/blockbuttonview.js index 890ad86a..5154de24 100644 --- a/tests/toolbar/block/view/blockbuttonview.js +++ b/tests/toolbar/block/view/blockbuttonview.js @@ -14,7 +14,7 @@ describe( 'BlockButtonView', () => { } ); it( 'should create element from template', () => { - expect( view.element.classList.contains( 'ck-toolbar-block-button' ) ).to.true; + expect( view.element.classList.contains( 'ck-block-toolbar-button' ) ).to.true; } ); describe( 'DOM binding', () => { From 90186d379a4ad2d3285457c0b89423a93b5d47ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 18 May 2018 13:15:39 +0200 Subject: [PATCH 13/57] Change: Changed block toolbar panel limiter. --- src/toolbar/block/blocktoolbar.js | 2 +- tests/toolbar/block/blocktoolbar.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index 9f657124..68953cb9 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -295,7 +295,7 @@ export default class BlockToolbar extends Plugin { _showPanel() { this.panelView.pin( { target: this.buttonView.element, - limiter: this.editor.ui.view.element + limiter: this.editor.ui.view.editableElement } ); } diff --git a/tests/toolbar/block/blocktoolbar.js b/tests/toolbar/block/blocktoolbar.js index f6c27132..48c74b42 100644 --- a/tests/toolbar/block/blocktoolbar.js +++ b/tests/toolbar/block/blocktoolbar.js @@ -171,7 +171,7 @@ describe( 'BlockToolbar', () => { expect( blockToolbar.panelView.isVisible ).to.true; sinon.assert.calledWith( spy, { target: blockToolbar.buttonView.element, - limiter: editor.ui.view.element + limiter: editor.ui.view.editableElement } ); } ); @@ -334,7 +334,7 @@ describe( 'BlockToolbar', () => { sinon.assert.calledWith( spy, { target: blockToolbar.buttonView.element, - limiter: editor.ui.view.element + limiter: editor.ui.view.editableElement } ); } ); From d7dd0b593c67a014cdfbd7fcecf9361533142527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 18 May 2018 13:16:03 +0200 Subject: [PATCH 14/57] Tests: Improved block toolbar manual test. --- tests/manual/blocktoolbar/blocktoolbar.html | 56 +++++++++++++++++--- tests/manual/blocktoolbar/blocktoolbar.js | 8 +-- tests/manual/blocktoolbar/umbrellas.jpg | Bin 0 -> 51250 bytes 3 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 tests/manual/blocktoolbar/umbrellas.jpg diff --git a/tests/manual/blocktoolbar/blocktoolbar.html b/tests/manual/blocktoolbar/blocktoolbar.html index ce09ffc0..55878a5d 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.html +++ b/tests/manual/blocktoolbar/blocktoolbar.html @@ -1,12 +1,56 @@
-

This is a first line of text.

-

This is a second line of text.

-

This is the end of text.

+

The three greatest things you learn from traveling

+

+ Like all the great things on earth traveling teaches us by example. Here are some of the most precious lessons + I’ve learned over the years of traveling. +

+ +
+ Three Monks walking on ancient temple. +
Leaving your comfort zone might lead you to such beautiful sceneries like this one.
+
+ +

Appreciation of diversity

+

+ Getting used to an entirely different culture can be challenging. While it’s also nice to learn about + cultures online or from books, nothing comes close to experiencing cultural diversity in person. + You learn to appreciate each and every single one of the differences while you become more culturally fluid. +

+ +
+

The real voyage of discovery consists not in seeking new landscapes, but having new eyes.

+

Marcel Proust

+
+ +

Improvisation

+

+ Life doesn't allow us to execute every single plan perfectly. This especially seems to be the case when + you travel. You plan it down to every minute with a big checklist; but when it comes to executing it, + something always comes up and you’re left with your improvising skills. You learn to adapt as you go. + Here’s how my travel checklist looks now: +

+ +
    +
  • buy the ticket
  • +
  • start your adventure
  • +
+ +

Confidence

+

+ Going to a new place can be quite terrifying. While change and uncertainty makes us scared, traveling + teaches us how ridiculous it is to be afraid of something before it happens. The moment you face your + fear and see there was nothing to be afraid of, is the moment you discover bliss. +

diff --git a/tests/manual/blocktoolbar/blocktoolbar.js b/tests/manual/blocktoolbar/blocktoolbar.js index 6d61fe7a..99d15955 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.js +++ b/tests/manual/blocktoolbar/blocktoolbar.js @@ -5,15 +5,17 @@ /* globals window, document, console:false */ -import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import BalloonEditor from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor'; import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; import HeadingButtonsUI from '@ckeditor/ckeditor5-heading/src/headingbuttonsui'; import ParagraphButtonUI from '@ckeditor/ckeditor5-paragraph/src/paragraphbuttonui'; +import BalloonToolbar from '../../../src/toolbar/balloon/balloontoolbar'; import BlockToolbar from '../../../src/toolbar/block/blocktoolbar'; -ClassicEditor +BalloonEditor .create( document.querySelector( '#editor' ), { - plugins: [ ArticlePluginSet, HeadingButtonsUI, ParagraphButtonUI, BlockToolbar ], + plugins: [ ArticlePluginSet, HeadingButtonsUI, ParagraphButtonUI, BalloonToolbar, BlockToolbar ], + balloonToolbar: [ 'link' ], blockToolbar: [ 'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', 'blockQuote' ] } ) .then( editor => { diff --git a/tests/manual/blocktoolbar/umbrellas.jpg b/tests/manual/blocktoolbar/umbrellas.jpg new file mode 100644 index 0000000000000000000000000000000000000000..66be0967f011b1b6eabe73e90b4d3a13ef8fb162 GIT binary patch literal 51250 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�upohh|0`s|#>1!gd?0DV9jdYfV9{sQ{`i* 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 Synk_c{Ujl;4?>eWAOG28O2(rA literal 0 HcmV?d00001 From a9db7dd492863c155cdbb895a8952f4169959c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 18 May 2018 13:18:27 +0200 Subject: [PATCH 15/57] Changed class name. --- tests/manual/blocktoolbar/blocktoolbar.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/manual/blocktoolbar/blocktoolbar.html b/tests/manual/blocktoolbar/blocktoolbar.html index 55878a5d..b058855a 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.html +++ b/tests/manual/blocktoolbar/blocktoolbar.html @@ -50,7 +50,7 @@

Confidence

max-width: 800px; } - .ck-toolbar-block-button { + .ck-block-toolbar-button { transform: translateX( 20px ); } From bab925287e85338f4b3b39648cf8eda621615e46 Mon Sep 17 00:00:00 2001 From: Damian Konopka Date: Fri, 18 May 2018 13:20:10 +0200 Subject: [PATCH 16/57] Typo. --- theme/components/toolbar/blocktoolbar.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theme/components/toolbar/blocktoolbar.css b/theme/components/toolbar/blocktoolbar.css index e909f684..59fa20c3 100644 --- a/theme/components/toolbar/blocktoolbar.css +++ b/theme/components/toolbar/blocktoolbar.css @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md. */ -.ck.ck-toolbar-block-button { +.ck.ck-block-toolbar-button { position: absolute; z-index: var(--ck-z-default); } From 2fce455c779731928d16fdfdbc27d08cb630d6d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 18 May 2018 14:41:10 +0200 Subject: [PATCH 17/57] Removed duplicated icon. --- src/toolbar/block/icons/pilcrow.svg | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/toolbar/block/icons/pilcrow.svg diff --git a/src/toolbar/block/icons/pilcrow.svg b/src/toolbar/block/icons/pilcrow.svg deleted file mode 100644 index 359434b7..00000000 --- a/src/toolbar/block/icons/pilcrow.svg +++ /dev/null @@ -1 +0,0 @@ - From 6f12abc8b397c213796640c85d8a725b8f58548c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 18 May 2018 14:41:49 +0200 Subject: [PATCH 18/57] Docs: Improved API docs. --- src/toolbar/block/blocktoolbar.js | 29 ++++++++++++++--------- src/toolbar/block/view/blockbuttonview.js | 4 ++++ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index 68953cb9..53266243 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -2,6 +2,10 @@ * Copyright (c) 2016 - 2017, CKSource - Frederico Knabben. All rights reserved. */ +/** + * @module ui/toolbar/block/blocktoolbar + */ + /* global window */ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; @@ -43,26 +47,26 @@ export default class BlockToolbar extends Plugin { /** * Toolbar view. * - * @type {ToolbarView} + * @type {module:ui/toolbar/toolbarview~ToolbarView} */ this.toolbarView = new ToolbarView( editor.locale ); /** * Panel view. * - * @type {BalloonPanelView} + * @type {module:ui/panel/balloon/balloonpanelview~BalloonPanelView} */ this.panelView = this._createPanelView(); /** * Button view. * - * @type {ButtonView} + * @type {module:ui/toolbar/block/view/blockbuttonview~BlockButtonView} */ this.buttonView = this._createButtonView(); /** - * List of block element names that allow do display toolbar next to it. + * List of block element names that allow displaying toolbar next to it. * This list will be updated by #afterInit method. * * @type {Array} @@ -114,7 +118,7 @@ export default class BlockToolbar extends Plugin { * Creates panel view. * * @private - * @returns {BalloonPanelView} + * @returns {module:ui/panel/balloon/balloonpanelview~BalloonPanelView} */ _createPanelView() { const editor = this.editor; @@ -138,7 +142,7 @@ export default class BlockToolbar extends Plugin { * Creates button view. * * @private - * @returns {BlockButtonView} + * @returns {module:ui/toolbar/block/view/blockbuttonview~BlockButtonView} */ _createButtonView() { const editor = this.editor; @@ -168,14 +172,17 @@ export default class BlockToolbar extends Plugin { } /** - * Returns list of element names that allow to display block button next to it. + * Returns list of element names that allow displaying block button next to it. * * @private + * @returns {Array} */ _getAllowedElements() { + const config = this.editor.config; + const elements = [ 'p', 'li' ]; - for ( const item of this.editor.config.get( 'heading.options' ) || [] ) { + for ( const item of config.get( 'heading.options' ) || [] ) { if ( item.view ) { elements.push( item.view ); } @@ -230,11 +237,12 @@ export default class BlockToolbar extends Plugin { } }, { priority: 'low' } ); - // Keep button and panel position on window#resize. this.listenTo( this.buttonView, 'change:isVisible', ( evt, name, isVisible ) => { if ( isVisible ) { + // Keep correct position of button and panel on window#resize. this.buttonView.listenTo( window, 'resize', () => this._attachButtonToElement( targetDomElement ) ); } else { + // Stop repositioning button when is hidden. this.buttonView.stopListening( window, 'resize' ); // Hide the panel when the button disappears. @@ -249,11 +257,10 @@ export default class BlockToolbar extends Plugin { * @private */ _disable() { + this.buttonView.isVisible = false; this.stopListening( this.editor.model.document.selection, 'change:range' ); this.stopListening( this.editor.editing.view, 'render' ); this.stopListening( this.buttonView, 'change:isVisible' ); - this.buttonView.isVisible = false; - this._hidePanel(); } /** diff --git a/src/toolbar/block/view/blockbuttonview.js b/src/toolbar/block/view/blockbuttonview.js index 53c05cbd..c4cc3c6a 100644 --- a/src/toolbar/block/view/blockbuttonview.js +++ b/src/toolbar/block/view/blockbuttonview.js @@ -2,6 +2,10 @@ * Copyright (c) 2016 - 2017, CKSource - Frederico Knabben. All rights reserved. */ +/** + * @module ui/toolbar/block/view/blockbuttonview + */ + import ButtonView from '../../../button/buttonview'; import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit'; import '../../../../theme/components/toolbar/blocktoolbar.css'; From 92e3851d0ea29739ba6421e9451f84fef6c8c6df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 18 May 2018 14:42:13 +0200 Subject: [PATCH 19/57] Tests: Added more toolbar items. --- tests/manual/blocktoolbar/blocktoolbar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/manual/blocktoolbar/blocktoolbar.js b/tests/manual/blocktoolbar/blocktoolbar.js index 99d15955..c98e0418 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.js +++ b/tests/manual/blocktoolbar/blocktoolbar.js @@ -15,7 +15,7 @@ import BlockToolbar from '../../../src/toolbar/block/blocktoolbar'; BalloonEditor .create( document.querySelector( '#editor' ), { plugins: [ ArticlePluginSet, HeadingButtonsUI, ParagraphButtonUI, BalloonToolbar, BlockToolbar ], - balloonToolbar: [ 'link' ], + balloonToolbar: [ 'bold', 'italic', 'link' ], blockToolbar: [ 'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', 'blockQuote' ] } ) .then( editor => { From 567bc8810462f699149befa3d4630a5b9c7dd2c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 18 May 2018 17:19:08 +0200 Subject: [PATCH 20/57] Added decorable method for checking if block toolbar is allowed + improved docs. --- src/toolbar/block/blocktoolbar.js | 64 +++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index 53266243..cee2259b 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -26,6 +26,35 @@ import iconPilcrow from '../../../theme/icons/pilcrow.svg'; /** * The block toolbar plugin. * + * This plugin provides button attached to the block of content where the selection is currently placed. + * After clicking on the button, dropdown with editor features defined through editor configuration appears. + * + * By default button is allowed to be displayed next to {@link module:paragraph/paragraph~Paragraph paragraph element}, + * {@link module:list/list~List list items} and all items defined in {@link module:heading/heading~Heading} plugin. + * This behavior can be customise through decorable {@link #checkAllowed} method. + * + * By default button will be attached to the left bound of the + * {@link module:ui/editorui/editoruiview~EditorUIView#editableElement} so editor integration should + * ensure that there is enough space between the editor content and left bound of the editable element + * + * | __ + * || | This is a block of content that + * | ¯¯ button is attached to. This is a + * | space block of content that button is + * | <-----> attached to. + * + * The position of the button can be adjusted using css transform: + * + * .ck-block-toolbar-button { + * transform: translate( 10px, 10px ); + * } + * + * | + * | __ This is a block of content that + * | | | button is attached to. This is a + * | ¯¯ block of content that button is + * | attached to. + * * @extends module:core/plugin~Plugin */ export default class BlockToolbar extends Plugin { @@ -90,6 +119,10 @@ export default class BlockToolbar extends Plugin { } } ); + // Checking if button is allowed for displaying next to given element is event–driven. + // It is possible to override #checkAllowed method and apply custom validation. + this.decorate( 'checkAllowed' ); + // Enable as default. this._enable(); } @@ -191,6 +224,31 @@ export default class BlockToolbar extends Plugin { return elements; } + /** + * Checks if block button is allowed for displaying next to given element. + * + * Fires {@link #event:checkAllowed} event which can be handled and overridden to apply custom validation. + * + * Example how to disallow button for `h1` element: + * + * const blockToolbar = editor.plugins.get( 'BlockToolbar' ); + * + * blockToolbar.on( 'checkAllowed', ( evt, args ) => { + * const viewElement = args[ 0 ]; + * + * if ( viewElement.name === 'h1' ) { + * evt.return = false; + * } + * }, { priority: 'high' } ); + * + * @fires checkAllowed + * @param {module:engine/view/containerelement~ContainerElement} viewElement Container element where selection is. + * @returns {Boolean} `true` when block button is allowed to display `false` otherwise. + */ + checkAllowed( viewElement ) { + return this._allowedElements.includes( viewElement.name ); + } + /** * Starts displaying button next to allowed elements. * @@ -213,10 +271,8 @@ export default class BlockToolbar extends Plugin { // Get selection parent container, block button will be attached to this element. targetElement = getParentContainer( viewDocument.selection.getFirstPosition() ); - const targetName = targetElement.name; - - // Do not attach block button when target element is not on the white list. - if ( !this._allowedElements.includes( targetName ) ) { + // Do not attach block button when is not allowed for given target element. + if ( !this.checkAllowed( targetElement ) ) { this.buttonView.isVisible = false; return; From fe875a574cd392317ba0677d58ab8b0422f1575e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 18 May 2018 17:28:42 +0200 Subject: [PATCH 21/57] Docs: Added missind event docs. --- src/toolbar/block/blocktoolbar.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index cee2259b..5f0b4df3 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -34,7 +34,7 @@ import iconPilcrow from '../../../theme/icons/pilcrow.svg'; * This behavior can be customise through decorable {@link #checkAllowed} method. * * By default button will be attached to the left bound of the - * {@link module:ui/editorui/editoruiview~EditorUIView#editableElement} so editor integration should + * {@link module:engine/view/editableelement~EditableElement} so editor integration should * ensure that there is enough space between the editor content and left bound of the editable element * * | __ @@ -375,6 +375,13 @@ export default class BlockToolbar extends Plugin { this.editor.editing.view.focus(); } } + + /** + * This event is fired just before #checkAllowed method is executed. It makes it possible to override + * default method behavior and provides a custom validation. + * + * @event checkAllowed + */ } // Because the engine.view.writer.getParentContainer is not exported here is a copy. From 421f1cadc720c9811969e3e1f8b6d28255f7095e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 18 May 2018 17:29:19 +0200 Subject: [PATCH 22/57] Tests: Improved CC. --- tests/toolbar/block/blocktoolbar.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/toolbar/block/blocktoolbar.js b/tests/toolbar/block/blocktoolbar.js index 48c74b42..d08ec993 100644 --- a/tests/toolbar/block/blocktoolbar.js +++ b/tests/toolbar/block/blocktoolbar.js @@ -252,6 +252,20 @@ describe( 'BlockToolbar', () => { expect( blockToolbar.buttonView.isVisible ).to.false; } ); + + it( 'should make it possible to provide custom validation', () => { + blockToolbar.on( 'checkAllowed', ( evt, args ) => { + const viewElement = args[ 0 ]; + + if ( viewElement.name === 'h1' ) { + evt.return = false; + } + } ); + + setData( editor.model, 'foo[]bar' ); + + expect( blockToolbar.buttonView.isVisible ).to.true; + } ); } ); describe( 'attaching button to the content', () => { From 33fd20582ad37c3d6b59ad32c17b600dd3ff0cc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 18 May 2018 17:42:45 +0200 Subject: [PATCH 23/57] Docs: Fixed invalid link. --- src/toolbar/block/blocktoolbar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index 5f0b4df3..708ae09e 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -31,7 +31,7 @@ import iconPilcrow from '../../../theme/icons/pilcrow.svg'; * * By default button is allowed to be displayed next to {@link module:paragraph/paragraph~Paragraph paragraph element}, * {@link module:list/list~List list items} and all items defined in {@link module:heading/heading~Heading} plugin. - * This behavior can be customise through decorable {@link #checkAllowed} method. + * This behavior can be customise through decorable {@link ~BlockToolbar#checkAllowed} method. * * By default button will be attached to the left bound of the * {@link module:engine/view/editableelement~EditableElement} so editor integration should @@ -377,7 +377,7 @@ export default class BlockToolbar extends Plugin { } /** - * This event is fired just before #checkAllowed method is executed. It makes it possible to override + * This event is fired just before {@link #checkAllowed} method is executed. It makes it possible to override * default method behavior and provides a custom validation. * * @event checkAllowed From 904fcd9a18ab00578027475b7107d7bb503ed7a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 18 May 2018 17:46:20 +0200 Subject: [PATCH 24/57] Tests: Fixed test that not check anything. --- tests/toolbar/block/blocktoolbar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/toolbar/block/blocktoolbar.js b/tests/toolbar/block/blocktoolbar.js index d08ec993..d15d4007 100644 --- a/tests/toolbar/block/blocktoolbar.js +++ b/tests/toolbar/block/blocktoolbar.js @@ -257,14 +257,14 @@ describe( 'BlockToolbar', () => { blockToolbar.on( 'checkAllowed', ( evt, args ) => { const viewElement = args[ 0 ]; - if ( viewElement.name === 'h1' ) { + if ( viewElement.name === 'h2' ) { evt.return = false; } } ); setData( editor.model, 'foo[]bar' ); - expect( blockToolbar.buttonView.isVisible ).to.true; + expect( blockToolbar.buttonView.isVisible ).to.false; } ); } ); From 57beddc54c8e8739f0855faf6c41511521e1a2b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 18 May 2018 17:46:39 +0200 Subject: [PATCH 25/57] Tests: Improved manual test. --- tests/manual/blocktoolbar/blocktoolbar.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/manual/blocktoolbar/blocktoolbar.js b/tests/manual/blocktoolbar/blocktoolbar.js index c98e0418..32d1de7f 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.js +++ b/tests/manual/blocktoolbar/blocktoolbar.js @@ -12,9 +12,23 @@ import ParagraphButtonUI from '@ckeditor/ckeditor5-paragraph/src/paragraphbutton import BalloonToolbar from '../../../src/toolbar/balloon/balloontoolbar'; import BlockToolbar from '../../../src/toolbar/block/blocktoolbar'; +class CustomBlockToolbar extends BlockToolbar { + init() { + super.init(); + + this.on( 'checkAllowed', ( evt, args ) => { + const viewElement = args[ 0 ]; + + if ( viewElement.name === 'h2' ) { + evt.return = false; + } + } ); + } +} + BalloonEditor .create( document.querySelector( '#editor' ), { - plugins: [ ArticlePluginSet, HeadingButtonsUI, ParagraphButtonUI, BalloonToolbar, BlockToolbar ], + plugins: [ ArticlePluginSet, HeadingButtonsUI, ParagraphButtonUI, BalloonToolbar, CustomBlockToolbar ], balloonToolbar: [ 'bold', 'italic', 'link' ], blockToolbar: [ 'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', 'blockQuote' ] } ) From d3c75e0d257fd67267e6849e8a98905c103f9e87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 18 May 2018 17:52:42 +0200 Subject: [PATCH 26/57] Tests: Added test cases to manual test. --- tests/manual/blocktoolbar/blocktoolbar.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/manual/blocktoolbar/blocktoolbar.md b/tests/manual/blocktoolbar/blocktoolbar.md index b24ba871..925a1f73 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.md +++ b/tests/manual/blocktoolbar/blocktoolbar.md @@ -1 +1,7 @@ ## Block toolbar demo + +1. Check if button appears next to all block elements except image (default) and heading1 (custom). +2. Change format of one of the block elements, panel attached to button should hide after that. +3. Put selection in the one of the last blocks, click on button to display panel then start resize browser window +and observe if button and panel are properly repositioned. + From bf0939caa59932d828223c94dd4a67d66e4fec38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 18 May 2018 18:04:58 +0200 Subject: [PATCH 27/57] Added missing dependencies. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 43444edc..318a6991 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@ckeditor/ckeditor5-basic-styles": "^10.0.0", "@ckeditor/ckeditor5-block-quote": "^10.0.0", "@ckeditor/ckeditor5-cloud-services": "^10.0.0", + "@ckeditor/ckeditor5-editor-balloon": "^10.0.0", "@ckeditor/ckeditor5-editor-classic": "^10.0.0", "@ckeditor/ckeditor5-engine": "^10.0.0", "@ckeditor/ckeditor5-enter": "^10.0.0", From 6a0f1ec74d593370f17eef39732be52726a2245f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 18 May 2018 18:40:33 +0200 Subject: [PATCH 28/57] Tests: Made tests more bulletproof. --- tests/toolbar/block/blocktoolbar.js | 46 ++++++++++++++++------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/tests/toolbar/block/blocktoolbar.js b/tests/toolbar/block/blocktoolbar.js index d15d4007..fecc5a61 100644 --- a/tests/toolbar/block/blocktoolbar.js +++ b/tests/toolbar/block/blocktoolbar.js @@ -21,6 +21,9 @@ import List from '@ckeditor/ckeditor5-list/src/list'; import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; + +testUtils.createSinonSandbox(); describe( 'BlockToolbar', () => { let editor, element, blockToolbar; @@ -87,7 +90,7 @@ describe( 'BlockToolbar', () => { } ); it( 'should close panelView after `Esc` press and focus view document', () => { - const spy = sinon.spy( editor.editing.view, 'focus' ); + const spy = testUtils.sinon.spy( editor.editing.view, 'focus' ); blockToolbar.panelView.isVisible = true; @@ -102,7 +105,7 @@ describe( 'BlockToolbar', () => { } ); it( 'should close panelView on click outside the panel and not focus view document', () => { - const spy = sinon.spy(); + const spy = testUtils.sinon.spy(); editor.editing.view.on( 'focus', spy ); blockToolbar.panelView.isVisible = true; @@ -164,7 +167,7 @@ describe( 'BlockToolbar', () => { it( 'should pin panelView to the button on #execute event', () => { expect( blockToolbar.panelView.isVisible ).to.false; - const spy = sinon.spy( blockToolbar.panelView, 'pin' ); + const spy = testUtils.sinon.spy( blockToolbar.panelView, 'pin' ); blockToolbar.buttonView.fire( 'execute' ); @@ -177,7 +180,7 @@ describe( 'BlockToolbar', () => { it( 'should hide panelView and focus editable on #execute event when panel was visible', () => { blockToolbar.panelView.isVisible = true; - const spy = sinon.spy( editor.editing.view, 'focus' ); + const spy = testUtils.sinon.spy( editor.editing.view, 'focus' ); blockToolbar.buttonView.fire( 'execute' ); @@ -270,6 +273,12 @@ describe( 'BlockToolbar', () => { describe( 'attaching button to the content', () => { it( 'should attach button to the left side of selected content and center with the first line on view#render #1', () => { + // Mock window dimensions. + testUtils.sinon.stub( window, 'innerWidth' ).value( 500 ); + testUtils.sinon.stub( window, 'innerHeight' ).value( 500 ); + testUtils.sinon.stub( window, 'scrollX' ).value( 0 ); + testUtils.sinon.stub( window, 'scrollY' ).value( 0 ); + setData( editor.model, 'foo[]bar' ); const target = editor.ui.view.editableElement.querySelector( 'p' ); @@ -277,16 +286,16 @@ describe( 'BlockToolbar', () => { target.style.lineHeight = '20px'; target.style.paddingTop = '10px'; - const editableRectSpy = sinon.stub( editor.ui.view.editableElement, 'getBoundingClientRect' ).returns( { + testUtils.sinon.stub( editor.ui.view.editableElement, 'getBoundingClientRect' ).returns( { left: 100 } ); - const targetRectSpy = sinon.stub( target, 'getBoundingClientRect' ).returns( { + testUtils.sinon.stub( target, 'getBoundingClientRect' ).returns( { top: 500, left: 300 } ); - const buttonRectSpy = sinon.stub( blockToolbar.buttonView.element, 'getBoundingClientRect' ).returns( { + testUtils.sinon.stub( blockToolbar.buttonView.element, 'getBoundingClientRect' ).returns( { width: 100, height: 100 } ); @@ -295,13 +304,14 @@ describe( 'BlockToolbar', () => { expect( blockToolbar.buttonView.top ).to.equal( 470 ); expect( blockToolbar.buttonView.left ).to.equal( 100 ); - - editableRectSpy.restore(); - targetRectSpy.restore(); - buttonRectSpy.restore(); } ); it( 'should attach button to the left side of selected content and center with the first line on view#render #2', () => { + testUtils.sinon.stub( window, 'innerWidth' ).value( 500 ); + testUtils.sinon.stub( window, 'innerHeight' ).value( 500 ); + testUtils.sinon.stub( window, 'scrollX' ).value( 0 ); + testUtils.sinon.stub( window, 'scrollY' ).value( 0 ); + setData( editor.model, 'foo[]bar' ); const target = editor.ui.view.editableElement.querySelector( 'p' ); @@ -309,16 +319,16 @@ describe( 'BlockToolbar', () => { target.style.fontSize = '20px'; target.style.paddingTop = '10px'; - const editableRectSpy = sinon.stub( editor.ui.view.editableElement, 'getBoundingClientRect' ).returns( { + testUtils.sinon.stub( editor.ui.view.editableElement, 'getBoundingClientRect' ).returns( { left: 100 } ); - const targetRectSpy = sinon.stub( target, 'getBoundingClientRect' ).returns( { + testUtils.sinon.stub( target, 'getBoundingClientRect' ).returns( { top: 500, left: 300 } ); - const buttonRectSpy = sinon.stub( blockToolbar.buttonView.element, 'getBoundingClientRect' ).returns( { + testUtils.sinon.stub( blockToolbar.buttonView.element, 'getBoundingClientRect' ).returns( { width: 100, height: 100 } ); @@ -327,16 +337,12 @@ describe( 'BlockToolbar', () => { expect( blockToolbar.buttonView.top ).to.equal( 470 ); expect( blockToolbar.buttonView.left ).to.equal( 100 ); - - editableRectSpy.restore(); - targetRectSpy.restore(); - buttonRectSpy.restore(); } ); it( 'should reposition panelView when is opened on view#render', () => { blockToolbar.panelView.isVisible = false; - const spy = sinon.spy( blockToolbar.panelView, 'pin' ); + const spy = testUtils.sinon.spy( blockToolbar.panelView, 'pin' ); editor.editing.view.fire( 'render' ); @@ -397,7 +403,7 @@ describe( 'BlockToolbar', () => { editor.model.schema.register( 'table', { inheritAllFrom: '$block' } ); editor.conversion.elementToElement( { model: 'table', view: 'table' } ); - const spy = sinon.spy( blockToolbar, '_attachButtonToElement' ); + const spy = testUtils.sinon.spy( blockToolbar, '_attachButtonToElement' ); setData( editor.model, 'fo[]o
bar' ); From 2cdcee41c818311c8fde98e4ee6203ab4328d806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Sat, 19 May 2018 21:38:39 +0200 Subject: [PATCH 29/57] Tests: Used getComputedStyle mock instead of setting styles. --- src/toolbar/block/blocktoolbar.js | 4 ++-- tests/toolbar/block/blocktoolbar.js | 29 ++++++++++++++--------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index 708ae09e..6a47f641 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -329,8 +329,8 @@ export default class BlockToolbar extends Plugin { const contentComputedStyles = window.getComputedStyle( targetElement ); const editableRect = new Rect( this.editor.ui.view.editableElement ); - const contentPaddingTop = parseInt( contentComputedStyles.paddingTop ); - const contentLineHeight = parseInt( contentComputedStyles.lineHeight ) || parseInt( contentComputedStyles.fontSize ); + const contentPaddingTop = parseInt( contentComputedStyles.paddingTop, 10 ); + const contentLineHeight = parseInt( contentComputedStyles.lineHeight, 10 ) || parseInt( contentComputedStyles.fontSize, 10 ); const position = getOptimalPosition( { element: this.buttonView.element, diff --git a/tests/toolbar/block/blocktoolbar.js b/tests/toolbar/block/blocktoolbar.js index fecc5a61..33eaa309 100644 --- a/tests/toolbar/block/blocktoolbar.js +++ b/tests/toolbar/block/blocktoolbar.js @@ -273,18 +273,17 @@ describe( 'BlockToolbar', () => { describe( 'attaching button to the content', () => { it( 'should attach button to the left side of selected content and center with the first line on view#render #1', () => { - // Mock window dimensions. - testUtils.sinon.stub( window, 'innerWidth' ).value( 500 ); - testUtils.sinon.stub( window, 'innerHeight' ).value( 500 ); - testUtils.sinon.stub( window, 'scrollX' ).value( 0 ); - testUtils.sinon.stub( window, 'scrollY' ).value( 0 ); - setData( editor.model, 'foo[]bar' ); const target = editor.ui.view.editableElement.querySelector( 'p' ); + const styleMock = testUtils.sinon.stub( window, 'getComputedStyle' ); + + styleMock.withArgs( target ).returns( { + lineHeight: '20px', + paddingTop: '10px' + } ); - target.style.lineHeight = '20px'; - target.style.paddingTop = '10px'; + styleMock.callThrough(); testUtils.sinon.stub( editor.ui.view.editableElement, 'getBoundingClientRect' ).returns( { left: 100 @@ -307,17 +306,17 @@ describe( 'BlockToolbar', () => { } ); it( 'should attach button to the left side of selected content and center with the first line on view#render #2', () => { - testUtils.sinon.stub( window, 'innerWidth' ).value( 500 ); - testUtils.sinon.stub( window, 'innerHeight' ).value( 500 ); - testUtils.sinon.stub( window, 'scrollX' ).value( 0 ); - testUtils.sinon.stub( window, 'scrollY' ).value( 0 ); - setData( editor.model, 'foo[]bar' ); const target = editor.ui.view.editableElement.querySelector( 'p' ); + const styleMock = testUtils.sinon.stub( window, 'getComputedStyle' ); + + styleMock.withArgs( target ).returns( { + fontSize: '20px', + paddingTop: '10px' + } ); - target.style.fontSize = '20px'; - target.style.paddingTop = '10px'; + styleMock.callThrough(); testUtils.sinon.stub( editor.ui.view.editableElement, 'getBoundingClientRect' ).returns( { left: 100 From eb6a3c41e99444b052268bf443c98e0c1d20276a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Sat, 19 May 2018 22:08:43 +0200 Subject: [PATCH 30/57] Docs: Added EditorConfig docs. --- src/toolbar/block/blocktoolbar.js | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index 6a47f641..26289c03 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -27,7 +27,8 @@ import iconPilcrow from '../../../theme/icons/pilcrow.svg'; * The block toolbar plugin. * * This plugin provides button attached to the block of content where the selection is currently placed. - * After clicking on the button, dropdown with editor features defined through editor configuration appears. + * After clicking on the button, dropdown with editor features defined through + * {@link module:core/editor/editorconfig~EditorConfig#blockToolbar} appears. * * By default button is allowed to be displayed next to {@link module:paragraph/paragraph~Paragraph paragraph element}, * {@link module:list/list~List list items} and all items defined in {@link module:heading/heading~Heading} plugin. @@ -35,7 +36,7 @@ import iconPilcrow from '../../../theme/icons/pilcrow.svg'; * * By default button will be attached to the left bound of the * {@link module:engine/view/editableelement~EditableElement} so editor integration should - * ensure that there is enough space between the editor content and left bound of the editable element + * ensure that there is enough space between the editor content and left bound of the editable element: * * | __ * || | This is a block of content that @@ -96,7 +97,7 @@ export default class BlockToolbar extends Plugin { /** * List of block element names that allow displaying toolbar next to it. - * This list will be updated by #afterInit method. + * This list will be updated by {@link ~BlockToolbar#afterInit} method. * * @type {Array} */ @@ -377,8 +378,8 @@ export default class BlockToolbar extends Plugin { } /** - * This event is fired just before {@link #checkAllowed} method is executed. It makes it possible to override - * default method behavior and provides a custom validation. + * This event is fired when {@link #checkAllowed} method is executed. It makes it possible to override + * default method behavior and provides a custom rules. * * @event checkAllowed */ @@ -395,3 +396,22 @@ function getParentContainer( position ) { return parent; } + +/** + * Block toolbar configuration. Used by the {@link module:ui/toolbar/block/blocktoolbar~BlockToolbar} + * feature. + * + * const config = { + * blockToolbar: [ 'paragraph', 'heading1', 'heading2', 'bulletedList', 'numberedList' ] + * }; + * + * You can also use `'|'` to create a separator between groups of items: + * + * const config = { + * blockToolbar: [ 'paragraph', 'heading1', 'heading2', '|', 'bulletedList', 'numberedList' ] + * }; + * + * Read also about configuring the main editor toolbar in {@link module:core/editor/editorconfig~EditorConfig#toolbar}. + * + * @member {Array.|Object} module:core/editor/editorconfig~EditorConfig#blockToolbar + */ From ff2c9911e72c2bac5adaab8fa7d7f1064cb957aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 21 May 2018 11:50:46 +0200 Subject: [PATCH 31/57] Focused toolbar on panel open. --- src/toolbar/block/blocktoolbar.js | 6 ++++++ tests/toolbar/block/blocktoolbar.js | 18 +++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index 26289c03..d1b499d4 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -357,10 +357,16 @@ export default class BlockToolbar extends Plugin { * @private */ _showPanel() { + const wasVisible = this.panelView.isVisible; + this.panelView.pin( { target: this.buttonView.element, limiter: this.editor.ui.view.editableElement } ); + + if ( !wasVisible ) { + this.toolbarView.items.get( 0 ).focus(); + } } /** diff --git a/tests/toolbar/block/blocktoolbar.js b/tests/toolbar/block/blocktoolbar.js index 33eaa309..574bb3a2 100644 --- a/tests/toolbar/block/blocktoolbar.js +++ b/tests/toolbar/block/blocktoolbar.js @@ -164,18 +164,20 @@ describe( 'BlockToolbar', () => { expect( editor.ui.focusTracker.isFocused ).to.true; } ); - it( 'should pin panelView to the button on #execute event', () => { + it( 'should pin panelView to the button and focus first item in toolbar on #execute event', () => { expect( blockToolbar.panelView.isVisible ).to.false; - const spy = testUtils.sinon.spy( blockToolbar.panelView, 'pin' ); + const pinSpy = testUtils.sinon.spy( blockToolbar.panelView, 'pin' ); + const focusSpy = testUtils.sinon.spy( blockToolbar.toolbarView.items.get( 0 ), 'focus' ); blockToolbar.buttonView.fire( 'execute' ); expect( blockToolbar.panelView.isVisible ).to.true; - sinon.assert.calledWith( spy, { + sinon.assert.calledWith( pinSpy, { target: blockToolbar.buttonView.element, limiter: editor.ui.view.editableElement } ); + sinon.assert.calledOnce( focusSpy ); } ); it( 'should hide panelView and focus editable on #execute event when panel was visible', () => { @@ -357,6 +359,16 @@ describe( 'BlockToolbar', () => { } ); } ); + it( 'should not reset toolbar focus on view#render', () => { + blockToolbar.panelView.isVisible = true; + + const spy = testUtils.sinon.spy( blockToolbar.toolbarView, 'focus' ); + + editor.editing.view.fire( 'render' ); + + sinon.assert.notCalled( spy ); + } ); + it( 'should hide opened panel on a selection direct change', () => { blockToolbar.panelView.isVisible = true; From aed85eb9526c089ecb8486533b49458ab8bfaaf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 21 May 2018 11:51:39 +0200 Subject: [PATCH 32/57] Tests: Added test case with external changes. --- tests/manual/blocktoolbar/blocktoolbar.html | 3 + tests/manual/blocktoolbar/blocktoolbar.js | 77 ++++++++++++++++++++- tests/manual/blocktoolbar/blocktoolbar.md | 13 ++++ 3 files changed, 92 insertions(+), 1 deletion(-) diff --git a/tests/manual/blocktoolbar/blocktoolbar.html b/tests/manual/blocktoolbar/blocktoolbar.html index b058855a..8b87ebe5 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.html +++ b/tests/manual/blocktoolbar/blocktoolbar.html @@ -1,3 +1,6 @@ + + +

The three greatest things you learn from traveling

diff --git a/tests/manual/blocktoolbar/blocktoolbar.js b/tests/manual/blocktoolbar/blocktoolbar.js index 32d1de7f..20011997 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.js +++ b/tests/manual/blocktoolbar/blocktoolbar.js @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md. */ -/* globals window, document, console:false */ +/* globals window, document, console:false, setTimeout */ import BalloonEditor from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor'; import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; @@ -12,6 +12,9 @@ import ParagraphButtonUI from '@ckeditor/ckeditor5-paragraph/src/paragraphbutton import BalloonToolbar from '../../../src/toolbar/balloon/balloontoolbar'; import BlockToolbar from '../../../src/toolbar/block/blocktoolbar'; +import Position from '@ckeditor/ckeditor5-engine/src/model/position'; +import Range from '@ckeditor/ckeditor5-engine/src/model/range'; + class CustomBlockToolbar extends BlockToolbar { init() { super.init(); @@ -34,7 +37,79 @@ BalloonEditor } ) .then( editor => { window.editor = editor; + + const externalChanges = createExternalChangesSimulator( editor ); + + document.querySelector( '.external-type' ).addEventListener( 'click', () => { + externalChanges.wait( 4000 ) + .then( () => externalChanges.insertNewLine( [ 1 ] ) ) + .then( () => externalChanges.type( [ 1, 0 ], 'New line' ) ) + .then( () => externalChanges.insertNewLine( [ 2 ] ) ) + .then( () => externalChanges.type( [ 2, 0 ], 'New line' ) ) + .then( () => externalChanges.insertNewLine( [ 3 ] ) ) + .then( () => externalChanges.type( [ 3, 0 ], 'New line' ) ); + } ); + + document.querySelector( '.external-delete' ).addEventListener( 'click', () => { + externalChanges.wait( 4000 ) + .then( () => externalChanges.removeElement( [ 1 ] ) ); + } ); } ) .catch( err => { console.error( err.stack ); } ); + +// Move it to the test utils. +// See https://github.com/ckeditor/ckeditor5-ui/issues/393. +function createExternalChangesSimulator( editor ) { + const { model } = editor; + + function wait( delay ) { + return new Promise( resolve => setTimeout( () => resolve(), delay ) ); + } + + function insertNewLine( path ) { + model.enqueueChange( 'transparent', writer => { + writer.insertElement( 'paragraph', new Position( model.document.getRoot(), path ) ); + } ); + + return Promise.resolve(); + } + + function type( path, text ) { + return new Promise( resolve => { + let position = new Position( model.document.getRoot(), path ); + let index = 0; + + function typing() { + wait( 40 ).then( () => { + model.enqueueChange( 'transparent', writer => { + writer.insertText( text[ index ], position ); + position = position.getShiftedBy( 1 ); + + const nextLetter = text[ ++index ]; + + if ( nextLetter ) { + typing( nextLetter ); + } else { + index = 0; + resolve(); + } + } ); + } ); + } + + typing(); + } ); + } + + function removeElement( path ) { + model.enqueueChange( 'transparent', writer => { + writer.remove( Range.createFromPositionAndShift( new Position( model.document.getRoot(), path ), 1 ) ); + } ); + + return Promise.resolve(); + } + + return { wait, insertNewLine, type, removeElement }; +} diff --git a/tests/manual/blocktoolbar/blocktoolbar.md b/tests/manual/blocktoolbar/blocktoolbar.md index 925a1f73..4ab12f7d 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.md +++ b/tests/manual/blocktoolbar/blocktoolbar.md @@ -5,3 +5,16 @@ 3. Put selection in the one of the last blocks, click on button to display panel then start resize browser window and observe if button and panel are properly repositioned. +### External changes + +## Typing + +1. Click `Start external typing` +2. Put selection to the first paragraph (`Like all the great things...`) and click the button to open panel (be quick). +3. Check if button and panel are repositioned correctly. + +## Removing + +1. Click `Start external deleting` +2. Put selection to the first paragraph (`Like all the great things...`) and click the button to open panel (be quick). +3. Check if button and panel are removed. From d79ecdba104b42ef85061e2025a9dec18343388f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 21 May 2018 23:43:25 +0200 Subject: [PATCH 33/57] Allowed BlockToolbar to be displayed next to $block elements. --- src/toolbar/block/blocktoolbar.js | 78 ++++++++++------------------- tests/toolbar/block/blocktoolbar.js | 59 +++++++--------------- 2 files changed, 43 insertions(+), 94 deletions(-) diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index d1b499d4..e87a5ddb 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -15,7 +15,6 @@ import BalloonPanelView from '../../panel/balloon/balloonpanelview'; import ToolbarView from '../toolbarview'; import ClickObserver from '@ckeditor/ckeditor5-engine/src/view/observer/clickobserver'; -import ContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement'; import clickOutsideHandler from '../../bindings/clickoutsidehandler'; import { getOptimalPosition } from '@ckeditor/ckeditor5-utils/src/dom/position'; @@ -95,14 +94,6 @@ export default class BlockToolbar extends Plugin { */ this.buttonView = this._createButtonView(); - /** - * List of block element names that allow displaying toolbar next to it. - * This list will be updated by {@link ~BlockToolbar#afterInit} method. - * - * @type {Array} - */ - this._allowedElements = []; - // Close #panelView on click out of the plugin UI. clickOutsideHandler( { emitter: this.panelView, @@ -144,8 +135,6 @@ export default class BlockToolbar extends Plugin { for ( const item of this.toolbarView.items ) { item.on( 'execute', () => this._hidePanel( true ), { priority: 'high' } ); } - - this._allowedElements = this._getAllowedElements(); } /** @@ -206,48 +195,31 @@ export default class BlockToolbar extends Plugin { } /** - * Returns list of element names that allow displaying block button next to it. - * - * @private - * @returns {Array} - */ - _getAllowedElements() { - const config = this.editor.config; - - const elements = [ 'p', 'li' ]; - - for ( const item of config.get( 'heading.options' ) || [] ) { - if ( item.view ) { - elements.push( item.view ); - } - } - - return elements; - } - - /** - * Checks if block button is allowed for displaying next to given element. + * Checks if block button is allowed for displaying next to given element + * (when element is a $block and is not an object). * * Fires {@link #event:checkAllowed} event which can be handled and overridden to apply custom validation. * - * Example how to disallow button for `h1` element: + * Example how to disallow button for `h2` element: * * const blockToolbar = editor.plugins.get( 'BlockToolbar' ); * * blockToolbar.on( 'checkAllowed', ( evt, args ) => { - * const viewElement = args[ 0 ]; + * const modelElement = args[ 0 ]; * - * if ( viewElement.name === 'h1' ) { + * if ( modelElement && modelElement.name === 'heading1' ) { * evt.return = false; * } * }, { priority: 'high' } ); * * @fires checkAllowed - * @param {module:engine/view/containerelement~ContainerElement} viewElement Container element where selection is. - * @returns {Boolean} `true` when block button is allowed to display `false` otherwise. + * @param {module:engine/model/element~Element} modelElement Element where the selection is. + * @returns {Boolean} `true` when block button is allowed to be displayed `false` otherwise. */ - checkAllowed( viewElement ) { - return this._allowedElements.includes( viewElement.name ); + checkAllowed( modelElement ) { + const schema = this.editor.model.schema; + + return modelElement && schema.isBlock( modelElement ) && !schema.isObject( modelElement ); } /** @@ -257,9 +229,9 @@ export default class BlockToolbar extends Plugin { */ _enable() { const editor = this.editor; + const model = editor.model; const view = editor.editing.view; - const viewDocument = view.document; - let targetElement, targetDomElement; + let modelTarget, domTarget; // Hides panel on a direct selection change. this.listenTo( editor.model.document.selection, 'change:range', ( evt, data ) => { @@ -269,24 +241,24 @@ export default class BlockToolbar extends Plugin { } ); this.listenTo( view, 'render', () => { - // Get selection parent container, block button will be attached to this element. - targetElement = getParentContainer( viewDocument.selection.getFirstPosition() ); + // Get selection closest parent block element, button will be attached to this element. + modelTarget = getParentBlock( model.document.selection.getFirstPosition(), model.schema ); // Do not attach block button when is not allowed for given target element. - if ( !this.checkAllowed( targetElement ) ) { + if ( !this.checkAllowed( modelTarget ) ) { this.buttonView.isVisible = false; return; } - // Get target DOM node. - targetDomElement = view.domConverter.mapViewToDom( targetElement ); + // Get DOM target element. + domTarget = view.domConverter.mapViewToDom( editor.editing.mapper.toViewElement( modelTarget ) ); // Show block button. this.buttonView.isVisible = true; // Attach block button to target DOM element. - this._attachButtonToElement( targetDomElement ); + this._attachButtonToElement( domTarget ); // When panel is opened then refresh it position to be properly aligned with block button. if ( this.panelView.isVisible ) { @@ -297,7 +269,7 @@ export default class BlockToolbar extends Plugin { this.listenTo( this.buttonView, 'change:isVisible', ( evt, name, isVisible ) => { if ( isVisible ) { // Keep correct position of button and panel on window#resize. - this.buttonView.listenTo( window, 'resize', () => this._attachButtonToElement( targetDomElement ) ); + this.buttonView.listenTo( window, 'resize', () => this._attachButtonToElement( domTarget ) ); } else { // Stop repositioning button when is hidden. this.buttonView.stopListening( window, 'resize' ); @@ -391,12 +363,14 @@ export default class BlockToolbar extends Plugin { */ } -// Because the engine.view.writer.getParentContainer is not exported here is a copy. -// See: https://github.com/ckeditor/ckeditor5-engine/issues/628 -function getParentContainer( position ) { +function getParentBlock( position, schema ) { let parent = position.parent; - while ( !( parent instanceof ContainerElement ) ) { + if ( parent.is( 'rootElement' ) ) { + return null; + } + + while ( !( schema.isBlock( parent ) ) ) { parent = parent.parent; } diff --git a/tests/toolbar/block/blocktoolbar.js b/tests/toolbar/block/blocktoolbar.js index 574bb3a2..2a8e85b2 100644 --- a/tests/toolbar/block/blocktoolbar.js +++ b/tests/toolbar/block/blocktoolbar.js @@ -17,6 +17,8 @@ import HeadingButtonsUI from '@ckeditor/ckeditor5-heading/src/headingbuttonsui'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import ParagraphButtonUI from '@ckeditor/ckeditor5-paragraph/src/paragraphbuttonui'; import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote'; +import Image from '@ckeditor/ckeditor5-image/src/image'; +import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption'; import List from '@ckeditor/ckeditor5-list/src/list'; import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; @@ -33,7 +35,7 @@ describe( 'BlockToolbar', () => { document.body.appendChild( element ); return ClassicTestEditor.create( element, { - plugins: [ BlockToolbar, Heading, HeadingButtonsUI, Paragraph, ParagraphButtonUI, BlockQuote, List ], + plugins: [ BlockToolbar, Heading, HeadingButtonsUI, Paragraph, ParagraphButtonUI, BlockQuote, Image, ImageCaption ], blockToolbar: [ 'paragraph', 'heading1', 'heading2', 'blockQuote' ] } ).then( newEditor => { editor = newEditor; @@ -213,56 +215,32 @@ describe( 'BlockToolbar', () => { } ); describe( 'allowed elements', () => { - it( 'should display button when selection is placed in a paragraph', () => { - setData( editor.model, 'foo[]bar' ); - - expect( blockToolbar.buttonView.isVisible ).to.true; - } ); + it( 'should display button when selection is placed in a block element', () => { + editor.model.schema.register( 'foo', { inheritAllFrom: '$block' } ); + editor.conversion.elementToElement( { model: 'foo', view: 'foo' } ); - it( 'should display button when selection is placed in a heading1', () => { - setData( editor.model, 'foo[]bar' ); + setData( editor.model, 'foo[]bar' ); expect( blockToolbar.buttonView.isVisible ).to.true; } ); - it( 'should display button when selection is placed in a heading2', () => { - setData( editor.model, 'foo[]bar' ); + it( 'should not display button when selection is placed in an object', () => { + setData( editor.model, 'fo[]o' ); - expect( blockToolbar.buttonView.isVisible ).to.true; - } ); - - it( 'should display button when selection is placed in a heading3', () => { - setData( editor.model, 'foo[]bar' ); - - expect( blockToolbar.buttonView.isVisible ).to.true; - } ); - - it( 'should display button when selection is placed in a list item', () => { - setData( editor.model, 'foo[]bar' ); - - expect( blockToolbar.buttonView.isVisible ).to.true; - } ); - - it( 'should display button when selection is placed in a allowed element in a blockQuote', () => { - setData( editor.model, '

foo[]bar
' ); - - expect( blockToolbar.buttonView.isVisible ).to.true; + expect( blockToolbar.buttonView.isVisible ).to.false; } ); - it( 'should not display button when selection is placed in not allowed element', () => { - editor.model.schema.register( 'table', { inheritAllFrom: '$block' } ); - editor.conversion.elementToElement( { model: 'table', view: 'table' } ); - - setData( editor.model, 'foo[]bar
' ); + it( 'should not display button when selection is placed in a root element', () => { + setData( editor.model, '[]' ); expect( blockToolbar.buttonView.isVisible ).to.false; } ); it( 'should make it possible to provide custom validation', () => { blockToolbar.on( 'checkAllowed', ( evt, args ) => { - const viewElement = args[ 0 ]; + const modelElement = args[ 0 ]; - if ( viewElement.name === 'h2' ) { + if ( modelElement.name === 'heading1' ) { evt.return = false; } } ); @@ -411,18 +389,15 @@ describe( 'BlockToolbar', () => { } ); it( 'should update button position on browser resize only when button is visible', () => { - editor.model.schema.register( 'table', { inheritAllFrom: '$block' } ); - editor.conversion.elementToElement( { model: 'table', view: 'table' } ); - const spy = testUtils.sinon.spy( blockToolbar, '_attachButtonToElement' ); - setData( editor.model, 'fo[]o
bar' ); + setData( editor.model, '[]bar' ); window.dispatchEvent( new Event( 'resize' ) ); sinon.assert.notCalled( spy ); - setData( editor.model, 'foo
ba[]r' ); + setData( editor.model, 'ba[]r' ); spy.resetHistory(); @@ -430,7 +405,7 @@ describe( 'BlockToolbar', () => { sinon.assert.called( spy ); - setData( editor.model, 'fo[]o
bar' ); + setData( editor.model, '[]bar' ); spy.resetHistory(); From 6b0b6ceb7c6136a37129625a5737f997879e333d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 21 May 2018 23:47:35 +0200 Subject: [PATCH 34/57] Improved BlockToolbar vertical aligning. --- src/toolbar/block/blocktoolbar.js | 9 ++++++--- tests/manual/blocktoolbar/blocktoolbar.html | 5 ----- tests/manual/blocktoolbar/blocktoolbar.js | 4 ++-- tests/toolbar/block/blocktoolbar.js | 3 ++- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index e87a5ddb..6f9cae20 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -299,11 +299,14 @@ export default class BlockToolbar extends Plugin { * @param {HTMLElement} targetElement Target element. */ _attachButtonToElement( targetElement ) { - const contentComputedStyles = window.getComputedStyle( targetElement ); + const contentStyles = window.getComputedStyle( targetElement ); const editableRect = new Rect( this.editor.ui.view.editableElement ); - const contentPaddingTop = parseInt( contentComputedStyles.paddingTop, 10 ); - const contentLineHeight = parseInt( contentComputedStyles.lineHeight, 10 ) || parseInt( contentComputedStyles.fontSize, 10 ); + const contentPaddingTop = parseInt( contentStyles.paddingTop, 10 ); + + // When line height is not an integer then thread it as "normal". + // MDN says that 'normal' == ~1.2 on desktop browsers. + const contentLineHeight = parseInt( contentStyles.lineHeight, 10 ) || parseInt( contentStyles.fontSize, 10 ) * 1.2; const position = getOptimalPosition( { element: this.buttonView.element, diff --git a/tests/manual/blocktoolbar/blocktoolbar.html b/tests/manual/blocktoolbar/blocktoolbar.html index 8b87ebe5..74812f10 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.html +++ b/tests/manual/blocktoolbar/blocktoolbar.html @@ -48,12 +48,7 @@

Confidence

diff --git a/tests/manual/blocktoolbar/blocktoolbar.js b/tests/manual/blocktoolbar/blocktoolbar.js index 20011997..d176c6b4 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.js +++ b/tests/manual/blocktoolbar/blocktoolbar.js @@ -20,9 +20,9 @@ class CustomBlockToolbar extends BlockToolbar { super.init(); this.on( 'checkAllowed', ( evt, args ) => { - const viewElement = args[ 0 ]; + const modelElement = args[ 0 ]; - if ( viewElement.name === 'h2' ) { + if ( modelElement && modelElement.name === 'heading1' ) { evt.return = false; } } ); diff --git a/tests/toolbar/block/blocktoolbar.js b/tests/toolbar/block/blocktoolbar.js index 2a8e85b2..cf40ee7a 100644 --- a/tests/toolbar/block/blocktoolbar.js +++ b/tests/toolbar/block/blocktoolbar.js @@ -292,6 +292,7 @@ describe( 'BlockToolbar', () => { const styleMock = testUtils.sinon.stub( window, 'getComputedStyle' ); styleMock.withArgs( target ).returns( { + lineHeight: 'normal', fontSize: '20px', paddingTop: '10px' } ); @@ -314,7 +315,7 @@ describe( 'BlockToolbar', () => { editor.editing.view.fire( 'render' ); - expect( blockToolbar.buttonView.top ).to.equal( 470 ); + expect( blockToolbar.buttonView.top ).to.equal( 472 ); expect( blockToolbar.buttonView.left ).to.equal( 100 ); } ); From 2b9b439ffe078865106b335cd6990e3c93c91290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 21 May 2018 23:52:11 +0200 Subject: [PATCH 35/57] Changed BlockToolbar horizontal position. --- src/toolbar/block/blocktoolbar.js | 2 +- tests/toolbar/block/blocktoolbar.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index 6f9cae20..520e1251 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -315,7 +315,7 @@ export default class BlockToolbar extends Plugin { ( contentRect, buttonRect ) => { return { top: contentRect.top + contentPaddingTop + ( ( contentLineHeight - buttonRect.height ) / 2 ), - left: editableRect.left + left: editableRect.left - buttonRect.width }; } ] diff --git a/tests/toolbar/block/blocktoolbar.js b/tests/toolbar/block/blocktoolbar.js index cf40ee7a..3cb58db7 100644 --- a/tests/toolbar/block/blocktoolbar.js +++ b/tests/toolbar/block/blocktoolbar.js @@ -266,7 +266,7 @@ describe( 'BlockToolbar', () => { styleMock.callThrough(); testUtils.sinon.stub( editor.ui.view.editableElement, 'getBoundingClientRect' ).returns( { - left: 100 + left: 200 } ); testUtils.sinon.stub( target, 'getBoundingClientRect' ).returns( { @@ -300,7 +300,7 @@ describe( 'BlockToolbar', () => { styleMock.callThrough(); testUtils.sinon.stub( editor.ui.view.editableElement, 'getBoundingClientRect' ).returns( { - left: 100 + left: 200 } ); testUtils.sinon.stub( target, 'getBoundingClientRect' ).returns( { From b7176d77666e92641d12f480306ed946ad1d3f62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Tue, 22 May 2018 08:30:11 +0200 Subject: [PATCH 36/57] Improved checking if BlockToolbar is allowed to be displayed. --- src/toolbar/block/blocktoolbar.js | 25 +++------------- tests/toolbar/block/blocktoolbar.js | 45 +++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index 520e1251..d054d5a2 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -217,9 +217,7 @@ export default class BlockToolbar extends Plugin { * @returns {Boolean} `true` when block button is allowed to be displayed `false` otherwise. */ checkAllowed( modelElement ) { - const schema = this.editor.model.schema; - - return modelElement && schema.isBlock( modelElement ) && !schema.isObject( modelElement ); + return modelElement && Array.from( this.toolbarView.items ).some( item => item.isEnabled ); } /** @@ -241,10 +239,10 @@ export default class BlockToolbar extends Plugin { } ); this.listenTo( view, 'render', () => { - // Get selection closest parent block element, button will be attached to this element. - modelTarget = getParentBlock( model.document.selection.getFirstPosition(), model.schema ); + // Get first selected block, button will be attached to this element. + modelTarget = Array.from( model.document.selection.getSelectedBlocks() )[ 0 ]; - // Do not attach block button when is not allowed for given target element. + // Do not attach block button when is not allowed for the given target element. if ( !this.checkAllowed( modelTarget ) ) { this.buttonView.isVisible = false; @@ -303,7 +301,6 @@ export default class BlockToolbar extends Plugin { const editableRect = new Rect( this.editor.ui.view.editableElement ); const contentPaddingTop = parseInt( contentStyles.paddingTop, 10 ); - // When line height is not an integer then thread it as "normal". // MDN says that 'normal' == ~1.2 on desktop browsers. const contentLineHeight = parseInt( contentStyles.lineHeight, 10 ) || parseInt( contentStyles.fontSize, 10 ) * 1.2; @@ -366,20 +363,6 @@ export default class BlockToolbar extends Plugin { */ } -function getParentBlock( position, schema ) { - let parent = position.parent; - - if ( parent.is( 'rootElement' ) ) { - return null; - } - - while ( !( schema.isBlock( parent ) ) ) { - parent = parent.parent; - } - - return parent; -} - /** * Block toolbar configuration. Used by the {@link module:ui/toolbar/block/blocktoolbar~BlockToolbar} * feature. diff --git a/tests/toolbar/block/blocktoolbar.js b/tests/toolbar/block/blocktoolbar.js index 3cb58db7..7a73a142 100644 --- a/tests/toolbar/block/blocktoolbar.js +++ b/tests/toolbar/block/blocktoolbar.js @@ -215,7 +215,7 @@ describe( 'BlockToolbar', () => { } ); describe( 'allowed elements', () => { - it( 'should display button when selection is placed in a block element', () => { + it( 'should display button when the first selected block is a block element', () => { editor.model.schema.register( 'foo', { inheritAllFrom: '$block' } ); editor.conversion.elementToElement( { model: 'foo', view: 'foo' } ); @@ -224,18 +224,45 @@ describe( 'BlockToolbar', () => { expect( blockToolbar.buttonView.isVisible ).to.true; } ); - it( 'should not display button when selection is placed in an object', () => { - setData( editor.model, 'fo[]o' ); + it( 'should display button when the first selected block is an object', () => { + setData( editor.model, '[foo]' ); - expect( blockToolbar.buttonView.isVisible ).to.false; + expect( blockToolbar.buttonView.isVisible ).to.true; + } ); + + it( 'should display button when the selection is inside the object', () => { + setData( editor.model, 'f[]oo' ); + + expect( blockToolbar.buttonView.isVisible ).to.true; } ); - it( 'should not display button when selection is placed in a root element', () => { - setData( editor.model, '[]' ); + it( 'should not display button when the selection is placed in a root element', () => { + setData( editor.model, 'foo[]bar' ); expect( blockToolbar.buttonView.isVisible ).to.false; } ); + it( 'should not display button when all toolbar items are disabled for the selected element', () => { + const element = document.createElement( 'div' ); + + document.body.appendChild( element ); + + return ClassicTestEditor.create( element, { + plugins: [ BlockToolbar, Heading, HeadingButtonsUI, Paragraph, ParagraphButtonUI, Image ], + blockToolbar: [ 'paragraph', 'heading1', 'heading2' ] + } ).then( editor => { + const blockToolbar = editor.plugins.get( BlockToolbar ); + + setData( editor.model, '[]' ); + + expect( blockToolbar.buttonView.isVisible ).to.false; + + element.remove(); + + return editor.destroy(); + } ); + } ); + it( 'should make it possible to provide custom validation', () => { blockToolbar.on( 'checkAllowed', ( evt, args ) => { const modelElement = args[ 0 ]; @@ -392,13 +419,13 @@ describe( 'BlockToolbar', () => { it( 'should update button position on browser resize only when button is visible', () => { const spy = testUtils.sinon.spy( blockToolbar, '_attachButtonToElement' ); - setData( editor.model, '[]bar' ); + setData( editor.model, '[]bar' ); window.dispatchEvent( new Event( 'resize' ) ); sinon.assert.notCalled( spy ); - setData( editor.model, 'ba[]r' ); + setData( editor.model, 'ba[]r' ); spy.resetHistory(); @@ -406,7 +433,7 @@ describe( 'BlockToolbar', () => { sinon.assert.called( spy ); - setData( editor.model, '[]bar' ); + setData( editor.model, '[]bar' ); spy.resetHistory(); From 48e8cced030b670226d4e471a8b06841e7b980e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Tue, 22 May 2018 08:34:29 +0200 Subject: [PATCH 37/57] Docs: Improved BlockToolbar general docs. --- src/toolbar/block/blocktoolbar.js | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index d054d5a2..53d97a16 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -29,31 +29,30 @@ import iconPilcrow from '../../../theme/icons/pilcrow.svg'; * After clicking on the button, dropdown with editor features defined through * {@link module:core/editor/editorconfig~EditorConfig#blockToolbar} appears. * - * By default button is allowed to be displayed next to {@link module:paragraph/paragraph~Paragraph paragraph element}, - * {@link module:list/list~List list items} and all items defined in {@link module:heading/heading~Heading} plugin. + * By default button is allowed to be displayed next to all elements marked in + * {@link module:engine/model/schema~Schema} as `$block` elements that are not `objects`. * This behavior can be customise through decorable {@link ~BlockToolbar#checkAllowed} method. * - * By default button will be attached to the left bound of the - * {@link module:engine/view/editableelement~EditableElement} so editor integration should - * ensure that there is enough space between the editor content and left bound of the editable element: + * By default button right bound will be attached to the left bound of the + * {@link module:engine/view/editableelement~EditableElement}: * - * | __ - * || | This is a block of content that - * | ¯¯ button is attached to. This is a - * | space block of content that button is - * | <-----> attached to. + * __ | + * | || This is a block of content that + * ¯¯ | button is attached to. This is a + * | block of content that button is + * | attached to. * * The position of the button can be adjusted using css transform: * * .ck-block-toolbar-button { - * transform: translate( 10px, 10px ); + * transform: translateX( -10px ); * } * - * | - * | __ This is a block of content that - * | | | button is attached to. This is a - * | ¯¯ block of content that button is - * | attached to. + * __ | + * | | | This is a block of content that + * ¯¯ | button is attached to. This is a + * | block of content that button is + * | attached to. * * @extends module:core/plugin~Plugin */ From ca02b84fdf2a726412d44131a41fdc0f8b69917c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Tue, 22 May 2018 08:34:57 +0200 Subject: [PATCH 38/57] Tests: Improved test names. --- tests/toolbar/block/blocktoolbar.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/toolbar/block/blocktoolbar.js b/tests/toolbar/block/blocktoolbar.js index 7a73a142..6ffe7c7c 100644 --- a/tests/toolbar/block/blocktoolbar.js +++ b/tests/toolbar/block/blocktoolbar.js @@ -279,7 +279,8 @@ describe( 'BlockToolbar', () => { } ); describe( 'attaching button to the content', () => { - it( 'should attach button to the left side of selected content and center with the first line on view#render #1', () => { + it( 'should attach right side of the button to the left side of the editable and center with the first line ' + + 'of selected block #1', () => { setData( editor.model, 'foo[]bar' ); const target = editor.ui.view.editableElement.querySelector( 'p' ); @@ -312,7 +313,8 @@ describe( 'BlockToolbar', () => { expect( blockToolbar.buttonView.left ).to.equal( 100 ); } ); - it( 'should attach button to the left side of selected content and center with the first line on view#render #2', () => { + it( 'should attach right side of the button to the left side of the editable and center with the first line ' + + 'of selected block #2', () => { setData( editor.model, 'foo[]bar' ); const target = editor.ui.view.editableElement.querySelector( 'p' ); From 0153af007d9581d5210c64c2e492f475e43dde92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Tue, 22 May 2018 08:35:25 +0200 Subject: [PATCH 39/57] Improved BlockToolbar manual test. --- tests/manual/blocktoolbar/blocktoolbar.html | 96 ++++++++++++--------- 1 file changed, 53 insertions(+), 43 deletions(-) diff --git a/tests/manual/blocktoolbar/blocktoolbar.html b/tests/manual/blocktoolbar/blocktoolbar.html index 74812f10..9ea8ecaa 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.html +++ b/tests/manual/blocktoolbar/blocktoolbar.html @@ -1,49 +1,51 @@ -
-

The three greatest things you learn from traveling

-

- Like all the great things on earth traveling teaches us by example. Here are some of the most precious lessons - I’ve learned over the years of traveling. -

- -
- Three Monks walking on ancient temple. -
Leaving your comfort zone might lead you to such beautiful sceneries like this one.
-
- -

Appreciation of diversity

-

- Getting used to an entirely different culture can be challenging. While it’s also nice to learn about - cultures online or from books, nothing comes close to experiencing cultural diversity in person. - You learn to appreciate each and every single one of the differences while you become more culturally fluid. -

- -
-

The real voyage of discovery consists not in seeking new landscapes, but having new eyes.

-

Marcel Proust

-
- -

Improvisation

-

- Life doesn't allow us to execute every single plan perfectly. This especially seems to be the case when - you travel. You plan it down to every minute with a big checklist; but when it comes to executing it, - something always comes up and you’re left with your improvising skills. You learn to adapt as you go. - Here’s how my travel checklist looks now: -

- -
    -
  • buy the ticket
  • -
  • start your adventure
  • -
- -

Confidence

-

- Going to a new place can be quite terrifying. While change and uncertainty makes us scared, traveling - teaches us how ridiculous it is to be afraid of something before it happens. The moment you face your - fear and see there was nothing to be afraid of, is the moment you discover bliss. -

+
+
+

The three greatest things you learn from traveling

+

+ Like all the great things on earth traveling teaches us by example. Here are some of the most precious lessons + I’ve learned over the years of traveling. +

+ +
+ Three Monks walking on ancient temple. +
Leaving your comfort zone might lead you to such beautiful sceneries like this one.
+
+ +

Appreciation of diversity

+

+ Getting used to an entirely different culture can be challenging. While it’s also nice to learn about + cultures online or from books, nothing comes close to experiencing cultural diversity in person. + You learn to appreciate each and every single one of the differences while you become more culturally fluid. +

+ +
+

The real voyage of discovery consists not in seeking new landscapes, but having new eyes.

+

Marcel Proust

+
+ +

Improvisation

+

+ Life doesn't allow us to execute every single plan perfectly. This especially seems to be the case when + you travel. You plan it down to every minute with a big checklist; but when it comes to executing it, + something always comes up and you’re left with your improvising skills. You learn to adapt as you go. + Here’s how my travel checklist looks now: +

+ +
    +
  • buy the ticket
  • +
  • start your adventure
  • +
+ +

Confidence

+

+ Going to a new place can be quite terrifying. While change and uncertainty makes us scared, traveling + teaches us how ridiculous it is to be afraid of something before it happens. The moment you face your + fear and see there was nothing to be afraid of, is the moment you discover bliss. +

+
From 13cd9eb1c4b3fe8413f56036dad059159db2cf53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Tue, 22 May 2018 13:44:33 +0200 Subject: [PATCH 40/57] Removed decorable method for disabling BlockToolbar. --- src/toolbar/block/blocktoolbar.js | 40 +++++------------------------ tests/toolbar/block/blocktoolbar.js | 28 -------------------- 2 files changed, 6 insertions(+), 62 deletions(-) diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index 53d97a16..17bce10c 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -30,8 +30,10 @@ import iconPilcrow from '../../../theme/icons/pilcrow.svg'; * {@link module:core/editor/editorconfig~EditorConfig#blockToolbar} appears. * * By default button is allowed to be displayed next to all elements marked in - * {@link module:engine/model/schema~Schema} as `$block` elements that are not `objects`. - * This behavior can be customise through decorable {@link ~BlockToolbar#checkAllowed} method. + * {@link module:engine/model/schema~Schema} as `$block` elements for which there is at least + * one available option in toolbar. E.g. Toolbar with {@link module:paragraph/paragraph~Paragraph} and + * {@link module:heading/heading~Heading} won't be displayed next to {@link module:image/image~Image} because + * {@link module:engine/model/schema~Schema} disallows to change format of {@link module:image/image~Image}. * * By default button right bound will be attached to the left bound of the * {@link module:engine/view/editableelement~EditableElement}: @@ -110,10 +112,6 @@ export default class BlockToolbar extends Plugin { } } ); - // Checking if button is allowed for displaying next to given element is event–driven. - // It is possible to override #checkAllowed method and apply custom validation. - this.decorate( 'checkAllowed' ); - // Enable as default. this._enable(); } @@ -193,32 +191,6 @@ export default class BlockToolbar extends Plugin { return buttonView; } - /** - * Checks if block button is allowed for displaying next to given element - * (when element is a $block and is not an object). - * - * Fires {@link #event:checkAllowed} event which can be handled and overridden to apply custom validation. - * - * Example how to disallow button for `h2` element: - * - * const blockToolbar = editor.plugins.get( 'BlockToolbar' ); - * - * blockToolbar.on( 'checkAllowed', ( evt, args ) => { - * const modelElement = args[ 0 ]; - * - * if ( modelElement && modelElement.name === 'heading1' ) { - * evt.return = false; - * } - * }, { priority: 'high' } ); - * - * @fires checkAllowed - * @param {module:engine/model/element~Element} modelElement Element where the selection is. - * @returns {Boolean} `true` when block button is allowed to be displayed `false` otherwise. - */ - checkAllowed( modelElement ) { - return modelElement && Array.from( this.toolbarView.items ).some( item => item.isEnabled ); - } - /** * Starts displaying button next to allowed elements. * @@ -241,8 +213,8 @@ export default class BlockToolbar extends Plugin { // Get first selected block, button will be attached to this element. modelTarget = Array.from( model.document.selection.getSelectedBlocks() )[ 0 ]; - // Do not attach block button when is not allowed for the given target element. - if ( !this.checkAllowed( modelTarget ) ) { + // Do not attach block button when there is no enabled item in toolbar for current block element. + if ( !modelTarget || Array.from( this.toolbarView.items ).every( item => !item.isEnabled ) ) { this.buttonView.isVisible = false; return; diff --git a/tests/toolbar/block/blocktoolbar.js b/tests/toolbar/block/blocktoolbar.js index 6ffe7c7c..677018a5 100644 --- a/tests/toolbar/block/blocktoolbar.js +++ b/tests/toolbar/block/blocktoolbar.js @@ -19,7 +19,6 @@ import ParagraphButtonUI from '@ckeditor/ckeditor5-paragraph/src/paragraphbutton import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote'; import Image from '@ckeditor/ckeditor5-image/src/image'; import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption'; -import List from '@ckeditor/ckeditor5-list/src/list'; import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; @@ -56,19 +55,6 @@ describe( 'BlockToolbar', () => { expect( editor.editing.view.getObserver( ClickObserver ) ).to.be.instanceOf( ClickObserver ); } ); - it( 'should initialize properly without Heading plugin', () => { - const element = document.createElement( 'div' ); - document.body.appendChild( element ); - - return ClassicTestEditor.create( element, { - plugins: [ BlockToolbar, Paragraph, ParagraphButtonUI, BlockQuote, List ], - blockToolbar: [ 'paragraph', 'blockQuote' ] - } ).then( editor => { - element.remove(); - return editor.destroy(); - } ); - } ); - describe( 'child views', () => { describe( 'panelView', () => { it( 'should create view instance', () => { @@ -262,20 +248,6 @@ describe( 'BlockToolbar', () => { return editor.destroy(); } ); } ); - - it( 'should make it possible to provide custom validation', () => { - blockToolbar.on( 'checkAllowed', ( evt, args ) => { - const modelElement = args[ 0 ]; - - if ( modelElement.name === 'heading1' ) { - evt.return = false; - } - } ); - - setData( editor.model, 'foo[]bar' ); - - expect( blockToolbar.buttonView.isVisible ).to.false; - } ); } ); describe( 'attaching button to the content', () => { From 0a6bc444487024c0ff6a7899060d02097a878f96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Tue, 22 May 2018 14:02:18 +0200 Subject: [PATCH 41/57] Tests: Adjusted manual test to last changes in BlockToolbar. --- tests/manual/blocktoolbar/blocktoolbar.html | 5 ---- tests/manual/blocktoolbar/blocktoolbar.js | 26 ++++++--------------- tests/manual/blocktoolbar/blocktoolbar.md | 2 +- 3 files changed, 8 insertions(+), 25 deletions(-) diff --git a/tests/manual/blocktoolbar/blocktoolbar.html b/tests/manual/blocktoolbar/blocktoolbar.html index 9ea8ecaa..1a5e65e4 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.html +++ b/tests/manual/blocktoolbar/blocktoolbar.html @@ -21,11 +21,6 @@

Appreciation of diversity

You learn to appreciate each and every single one of the differences while you become more culturally fluid.

-
-

The real voyage of discovery consists not in seeking new landscapes, but having new eyes.

-

Marcel Proust

-
-

Improvisation

Life doesn't allow us to execute every single plan perfectly. This especially seems to be the case when diff --git a/tests/manual/blocktoolbar/blocktoolbar.js b/tests/manual/blocktoolbar/blocktoolbar.js index d176c6b4..ddf83bca 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.js +++ b/tests/manual/blocktoolbar/blocktoolbar.js @@ -6,34 +6,22 @@ /* globals window, document, console:false, setTimeout */ import BalloonEditor from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor'; -import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; +import List from '@ckeditor/ckeditor5-list/src/list'; +import Image from '@ckeditor/ckeditor5-image/src/image'; +import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import Heading from '@ckeditor/ckeditor5-heading/src/heading'; import HeadingButtonsUI from '@ckeditor/ckeditor5-heading/src/headingbuttonsui'; import ParagraphButtonUI from '@ckeditor/ckeditor5-paragraph/src/paragraphbuttonui'; -import BalloonToolbar from '../../../src/toolbar/balloon/balloontoolbar'; import BlockToolbar from '../../../src/toolbar/block/blocktoolbar'; import Position from '@ckeditor/ckeditor5-engine/src/model/position'; import Range from '@ckeditor/ckeditor5-engine/src/model/range'; -class CustomBlockToolbar extends BlockToolbar { - init() { - super.init(); - - this.on( 'checkAllowed', ( evt, args ) => { - const modelElement = args[ 0 ]; - - if ( modelElement && modelElement.name === 'heading1' ) { - evt.return = false; - } - } ); - } -} - BalloonEditor .create( document.querySelector( '#editor' ), { - plugins: [ ArticlePluginSet, HeadingButtonsUI, ParagraphButtonUI, BalloonToolbar, CustomBlockToolbar ], - balloonToolbar: [ 'bold', 'italic', 'link' ], - blockToolbar: [ 'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', 'blockQuote' ] + plugins: [ List, Paragraph, Heading, Image, ImageCaption, HeadingButtonsUI, ParagraphButtonUI, BlockToolbar ], + blockToolbar: [ 'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList' ] } ) .then( editor => { window.editor = editor; diff --git a/tests/manual/blocktoolbar/blocktoolbar.md b/tests/manual/blocktoolbar/blocktoolbar.md index 4ab12f7d..f296db10 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.md +++ b/tests/manual/blocktoolbar/blocktoolbar.md @@ -1,6 +1,6 @@ ## Block toolbar demo -1. Check if button appears next to all block elements except image (default) and heading1 (custom). +1. Check if button appears next to all block elements except image (no toolbar item available). 2. Change format of one of the block elements, panel attached to button should hide after that. 3. Put selection in the one of the last blocks, click on button to display panel then start resize browser window and observe if button and panel are properly repositioned. From 8052d29bc261fb4986cb38c01c91d0312cbcc38c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Tue, 22 May 2018 14:20:34 +0200 Subject: [PATCH 42/57] Refactored handling read-only by BlockToolbar mode. --- src/toolbar/block/blocktoolbar.js | 28 ++++++--------------- tests/manual/blocktoolbar/blocktoolbar.html | 1 + tests/manual/blocktoolbar/blocktoolbar.js | 4 +++ tests/toolbar/block/blocktoolbar.js | 21 ++++++++-------- 4 files changed, 23 insertions(+), 31 deletions(-) diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index 17bce10c..ef1e9e9d 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -103,17 +103,17 @@ export default class BlockToolbar extends Plugin { callback: () => this._hidePanel() } ); - // Hide plugin UI when editor switch to read-only. - this.listenTo( editor, 'change:isReadOnly', ( evt, name, isReadOnly ) => { - if ( isReadOnly ) { - this._disable(); - } else { - this._enable(); + // Try to hide button when editor switch to read-only. + // Do not hide when panel was visible to avoid confusing situation when + // UI unexpectedly disappears. + this.listenTo( editor, 'change:isReadOnly', () => { + if ( !this.panelView.isVisible ) { + this.buttonView.isVisible = false; } } ); // Enable as default. - this._enable(); + this._initListeners(); } /** @@ -196,7 +196,7 @@ export default class BlockToolbar extends Plugin { * * @private */ - _enable() { + _initListeners() { const editor = this.editor; const model = editor.model; const view = editor.editing.view; @@ -249,18 +249,6 @@ export default class BlockToolbar extends Plugin { } ); } - /** - * Stops displaying block button. - * - * @private - */ - _disable() { - this.buttonView.isVisible = false; - this.stopListening( this.editor.model.document.selection, 'change:range' ); - this.stopListening( this.editor.editing.view, 'render' ); - this.stopListening( this.buttonView, 'change:isVisible' ); - } - /** * Attaches #buttonView to the target block of content. * diff --git a/tests/manual/blocktoolbar/blocktoolbar.html b/tests/manual/blocktoolbar/blocktoolbar.html index 1a5e65e4..b7ea39fe 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.html +++ b/tests/manual/blocktoolbar/blocktoolbar.html @@ -1,5 +1,6 @@ +

diff --git a/tests/manual/blocktoolbar/blocktoolbar.js b/tests/manual/blocktoolbar/blocktoolbar.js index ddf83bca..ab6c7233 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.js +++ b/tests/manual/blocktoolbar/blocktoolbar.js @@ -42,6 +42,10 @@ BalloonEditor externalChanges.wait( 4000 ) .then( () => externalChanges.removeElement( [ 1 ] ) ); } ); + + document.querySelector( '.read-only' ).addEventListener( 'click', () => { + editor.isReadOnly = !editor.isReadOnly; + } ); } ) .catch( err => { console.error( err.stack ); diff --git a/tests/toolbar/block/blocktoolbar.js b/tests/toolbar/block/blocktoolbar.js index 677018a5..5b6e1510 100644 --- a/tests/toolbar/block/blocktoolbar.js +++ b/tests/toolbar/block/blocktoolbar.js @@ -365,29 +365,28 @@ describe( 'BlockToolbar', () => { expect( blockToolbar.panelView.isVisible ).to.true; } ); - it( 'should hide button and stop attaching it when editor switch to readonly', () => { + it( 'should hide UI when editor switch to readonly when panel is not visible', () => { setData( editor.model, 'foo[]bar' ); - blockToolbar.panelView.isVisible = true; - - expect( blockToolbar.buttonView.isVisible ).to.true; - expect( blockToolbar.panelView.isVisible ).to.true; + blockToolbar.buttonView.isVisible = true; + blockToolbar.panelView.isVisible = false; editor.isReadOnly = true; expect( blockToolbar.buttonView.isVisible ).to.false; expect( blockToolbar.panelView.isVisible ).to.false; + } ); - editor.editing.view.fire( 'render' ); + it( 'should not hide button when editor switch to readonly when panel is visible', () => { + setData( editor.model, 'foo[]bar' ); - expect( blockToolbar.buttonView.isVisible ).to.false; - expect( blockToolbar.panelView.isVisible ).to.false; + blockToolbar.buttonView.isVisible = true; + blockToolbar.panelView.isVisible = true; - editor.isReadOnly = false; - editor.editing.view.fire( 'render' ); + editor.isReadOnly = true; expect( blockToolbar.buttonView.isVisible ).to.true; - expect( blockToolbar.panelView.isVisible ).to.false; + expect( blockToolbar.panelView.isVisible ).to.true; } ); it( 'should update button position on browser resize only when button is visible', () => { From 685364c85b6fd28ebdb37654ae962e38b357d3b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Tue, 22 May 2018 14:23:55 +0200 Subject: [PATCH 43/57] Tests: Minor improvements in manual test. --- tests/manual/blocktoolbar/blocktoolbar.html | 1 - tests/manual/blocktoolbar/blocktoolbar.js | 7 ++----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/manual/blocktoolbar/blocktoolbar.html b/tests/manual/blocktoolbar/blocktoolbar.html index b7ea39fe..1a5e65e4 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.html +++ b/tests/manual/blocktoolbar/blocktoolbar.html @@ -1,6 +1,5 @@ -
diff --git a/tests/manual/blocktoolbar/blocktoolbar.js b/tests/manual/blocktoolbar/blocktoolbar.js index ab6c7233..a6ce361c 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.js +++ b/tests/manual/blocktoolbar/blocktoolbar.js @@ -6,6 +6,7 @@ /* globals window, document, console:false, setTimeout */ import BalloonEditor from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor'; +import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials'; import List from '@ckeditor/ckeditor5-list/src/list'; import Image from '@ckeditor/ckeditor5-image/src/image'; import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption'; @@ -20,7 +21,7 @@ import Range from '@ckeditor/ckeditor5-engine/src/model/range'; BalloonEditor .create( document.querySelector( '#editor' ), { - plugins: [ List, Paragraph, Heading, Image, ImageCaption, HeadingButtonsUI, ParagraphButtonUI, BlockToolbar ], + plugins: [ Essentials, List, Paragraph, Heading, Image, ImageCaption, HeadingButtonsUI, ParagraphButtonUI, BlockToolbar ], blockToolbar: [ 'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList' ] } ) .then( editor => { @@ -42,10 +43,6 @@ BalloonEditor externalChanges.wait( 4000 ) .then( () => externalChanges.removeElement( [ 1 ] ) ); } ); - - document.querySelector( '.read-only' ).addEventListener( 'click', () => { - editor.isReadOnly = !editor.isReadOnly; - } ); } ) .catch( err => { console.error( err.stack ); From 3e2cb205007e34085d436bb0b54dd014c137221a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Tue, 22 May 2018 14:42:52 +0200 Subject: [PATCH 44/57] Added missing dev dependency. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 318a6991..ee9f24f7 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@ckeditor/ckeditor5-engine": "^10.0.0", "@ckeditor/ckeditor5-enter": "^10.0.0", "@ckeditor/ckeditor5-easy-image": "^10.0.0", + "@ckeditor/ckeditor5-essentials": "^10.0.0", "@ckeditor/ckeditor5-heading": "^10.0.0", "@ckeditor/ckeditor5-image": "^10.0.0", "@ckeditor/ckeditor5-link": "^10.0.0", From 1a6e1451a671f71a060f8e5d3f064819dcea4a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Tue, 22 May 2018 16:58:16 +0200 Subject: [PATCH 45/57] Docs: Added missing view docs. --- src/toolbar/block/view/blockbuttonview.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/toolbar/block/view/blockbuttonview.js b/src/toolbar/block/view/blockbuttonview.js index c4cc3c6a..23b72e5e 100644 --- a/src/toolbar/block/view/blockbuttonview.js +++ b/src/toolbar/block/view/blockbuttonview.js @@ -15,6 +15,10 @@ const toPx = toUnit( 'px' ); /** * The block button view class. * + * This view represents button that will be attached next to block element where the selection is placed. + * + * See {@link module:ui/toolbar/block/blocktoolbar~BlockToolbar}. + * * @extends {module:ui/button/buttonview~ButtonView} */ export default class BlockButtonView extends ButtonView { From b2aad6da00fe3d35565245b404e04df4676a39c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Tue, 22 May 2018 17:03:49 +0200 Subject: [PATCH 46/57] Added additional class to the ToolbarView in BlockToolbar. --- src/toolbar/block/blocktoolbar.js | 23 ++++++++++++++++++++++- tests/toolbar/block/blocktoolbar.js | 4 ++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index ef1e9e9d..a5aa4966 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -79,7 +79,7 @@ export default class BlockToolbar extends Plugin { * * @type {module:ui/toolbar/toolbarview~ToolbarView} */ - this.toolbarView = new ToolbarView( editor.locale ); + this.toolbarView = this._createToolbarView(); /** * Panel view. @@ -134,6 +134,27 @@ export default class BlockToolbar extends Plugin { } } + /** + * Creates toolbar view. + * + * @private + * @returns {module:ui/toolbar/toolbarview~ToolbarView} + */ + _createToolbarView() { + const toolbarView = new ToolbarView( this.editor.locale ); + + toolbarView.extendTemplate( { + attributes: { + class: [ + // https://github.com/ckeditor/ckeditor5-editor-inline/issues/11 + 'ck-toolbar_floating' + ] + } + } ); + + return toolbarView; + } + /** * Creates panel view. * diff --git a/tests/toolbar/block/blocktoolbar.js b/tests/toolbar/block/blocktoolbar.js index 5b6e1510..3a3283e7 100644 --- a/tests/toolbar/block/blocktoolbar.js +++ b/tests/toolbar/block/blocktoolbar.js @@ -116,6 +116,10 @@ describe( 'BlockToolbar', () => { expect( blockToolbar.toolbarView ).to.instanceof( ToolbarView ); } ); + it( 'should add additional class to toolbar element', () => { + expect( blockToolbar.toolbarView.element.classList.contains( 'ck-toolbar_floating' ) ).to.true; + } ); + it( 'should be added to panelView#content collection', () => { expect( Array.from( blockToolbar.panelView.content ) ).to.include( blockToolbar.toolbarView ); } ); From c05c836531aeddca88e9a34f8a06ccf1d1c4baed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Tue, 22 May 2018 17:12:07 +0200 Subject: [PATCH 47/57] Changed additional BalloonPanelView class. --- src/toolbar/block/blocktoolbar.js | 2 +- tests/toolbar/block/blocktoolbar.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index a5aa4966..d90373fb 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -166,7 +166,7 @@ export default class BlockToolbar extends Plugin { const panelView = new BalloonPanelView( editor.locale ); panelView.content.add( this.toolbarView ); - panelView.className = 'ck-balloon-panel-block-toolbar'; + panelView.className = 'ck-toolbar-container'; editor.ui.view.body.add( panelView ); editor.ui.focusTracker.add( panelView.element ); diff --git a/tests/toolbar/block/blocktoolbar.js b/tests/toolbar/block/blocktoolbar.js index 3a3283e7..cb1b0501 100644 --- a/tests/toolbar/block/blocktoolbar.js +++ b/tests/toolbar/block/blocktoolbar.js @@ -62,7 +62,7 @@ describe( 'BlockToolbar', () => { } ); it( 'should have additional class name', () => { - expect( blockToolbar.panelView.className ).to.equal( 'ck-balloon-panel-block-toolbar' ); + expect( blockToolbar.panelView.className ).to.equal( 'ck-toolbar-container' ); } ); it( 'should be added to ui.view.body collection', () => { From c241cc97d4d04230751a7189106d7fef429c92b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 23 May 2018 17:04:41 +0200 Subject: [PATCH 48/57] Hide BlockButton on init. --- src/toolbar/block/view/blockbuttonview.js | 3 +++ tests/toolbar/block/view/blockbuttonview.js | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/src/toolbar/block/view/blockbuttonview.js b/src/toolbar/block/view/blockbuttonview.js index 23b72e5e..97994030 100644 --- a/src/toolbar/block/view/blockbuttonview.js +++ b/src/toolbar/block/view/blockbuttonview.js @@ -30,6 +30,9 @@ export default class BlockButtonView extends ButtonView { const bind = this.bindTemplate; + // Hide button on init. + this.isVisible = false; + /** * Top offset. * diff --git a/tests/toolbar/block/view/blockbuttonview.js b/tests/toolbar/block/view/blockbuttonview.js index 5154de24..e7f65217 100644 --- a/tests/toolbar/block/view/blockbuttonview.js +++ b/tests/toolbar/block/view/blockbuttonview.js @@ -13,6 +13,10 @@ describe( 'BlockButtonView', () => { view.render(); } ); + it( 'should be not visible on init', () => { + expect( view.isVisible ).to.false; + } ); + it( 'should create element from template', () => { expect( view.element.classList.contains( 'ck-block-toolbar-button' ) ).to.true; } ); From bd8c2b3d47f03a618b1f81fb074d5e056d047e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Thu, 24 May 2018 22:51:58 +0200 Subject: [PATCH 49/57] Docs: Minor change. --- src/toolbar/block/blocktoolbar.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index d90373fb..fb587be6 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -31,9 +31,7 @@ import iconPilcrow from '../../../theme/icons/pilcrow.svg'; * * By default button is allowed to be displayed next to all elements marked in * {@link module:engine/model/schema~Schema} as `$block` elements for which there is at least - * one available option in toolbar. E.g. Toolbar with {@link module:paragraph/paragraph~Paragraph} and - * {@link module:heading/heading~Heading} won't be displayed next to {@link module:image/image~Image} because - * {@link module:engine/model/schema~Schema} disallows to change format of {@link module:image/image~Image}. + * one available (enable) option in the toolbar. * * By default button right bound will be attached to the left bound of the * {@link module:engine/view/editableelement~EditableElement}: From 2b807fc144e34cead0a2ec4ab988cfc954253839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 28 May 2018 09:12:15 +0200 Subject: [PATCH 50/57] Tests: Changed test case of manual test. --- tests/manual/blocktoolbar/blocktoolbar.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/manual/blocktoolbar/blocktoolbar.md b/tests/manual/blocktoolbar/blocktoolbar.md index f296db10..fb282e5b 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.md +++ b/tests/manual/blocktoolbar/blocktoolbar.md @@ -17,4 +17,4 @@ and observe if button and panel are properly repositioned. 1. Click `Start external deleting` 2. Put selection to the first paragraph (`Like all the great things...`) and click the button to open panel (be quick). -3. Check if button and panel are removed. +3. Check if button and panel are reattached. From d0c84805041c6bb3d4fdedcf0f2dd921a23f49ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 28 May 2018 10:13:39 +0200 Subject: [PATCH 51/57] Docs: Removed obsolete event docs. --- src/toolbar/block/blocktoolbar.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index fb587be6..faf8dd09 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -332,13 +332,6 @@ export default class BlockToolbar extends Plugin { this.editor.editing.view.focus(); } } - - /** - * This event is fired when {@link #checkAllowed} method is executed. It makes it possible to override - * default method behavior and provides a custom rules. - * - * @event checkAllowed - */ } /** From ec3caaa9ff402eaf68255819fd2b53c7fdff9053 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 28 May 2018 15:57:43 +0200 Subject: [PATCH 52/57] Docs: Improved docs in the BlockToolbar plugin. --- src/toolbar/block/blocktoolbar.js | 66 ++++++++++++++++--------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index faf8dd09..5058de7b 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -25,15 +25,14 @@ import iconPilcrow from '../../../theme/icons/pilcrow.svg'; /** * The block toolbar plugin. * - * This plugin provides button attached to the block of content where the selection is currently placed. - * After clicking on the button, dropdown with editor features defined through - * {@link module:core/editor/editorconfig~EditorConfig#blockToolbar} appears. + * This plugin provides the button positioned next to the block of content where the selection is anchored. + * Upon clicking the button, a drop–down providing editor features shows up, as configured in + * {@link module:core/editor/editorconfig~EditorConfig#blockToolbar}. * - * By default button is allowed to be displayed next to all elements marked in - * {@link module:engine/model/schema~Schema} as `$block` elements for which there is at least - * one available (enable) option in the toolbar. + * By default, the button is displayed next to all elements marked in {@link module:engine/model/schema~Schema} + * as `$block` for which the toolbar provides at least one option. * - * By default button right bound will be attached to the left bound of the + * By default, the button is attached so its right boundary is touching the * {@link module:engine/view/editableelement~EditableElement}: * * __ | @@ -42,7 +41,7 @@ import iconPilcrow from '../../../theme/icons/pilcrow.svg'; * | block of content that button is * | attached to. * - * The position of the button can be adjusted using css transform: + * The position of the button can be adjusted using the CSS `transform`: * * .ck-block-toolbar-button { * transform: translateX( -10px ); @@ -73,27 +72,27 @@ export default class BlockToolbar extends Plugin { editor.editing.view.addObserver( ClickObserver ); /** - * Toolbar view. + * The toolbar view. * * @type {module:ui/toolbar/toolbarview~ToolbarView} */ this.toolbarView = this._createToolbarView(); /** - * Panel view. + * The balloon panel view, containing the {@link #toolbarView}. * * @type {module:ui/panel/balloon/balloonpanelview~BalloonPanelView} */ this.panelView = this._createPanelView(); /** - * Button view. + * The button view, that opens the {@link #toolbarView}. * * @type {module:ui/toolbar/block/view/blockbuttonview~BlockButtonView} */ this.buttonView = this._createButtonView(); - // Close #panelView on click out of the plugin UI. + // Close the #panelView upon clicking outside of the plugin UI. clickOutsideHandler( { emitter: this.panelView, contextElements: [ this.panelView.element, this.buttonView.element ], @@ -101,9 +100,9 @@ export default class BlockToolbar extends Plugin { callback: () => this._hidePanel() } ); - // Try to hide button when editor switch to read-only. - // Do not hide when panel was visible to avoid confusing situation when - // UI unexpectedly disappears. + // Try to hide button when the editor switches to the read-only mode. + // Do not hide when panel if already visible to avoid a confusing UX when the panel + // unexpectedly disappears. this.listenTo( editor, 'change:isReadOnly', () => { if ( !this.panelView.isVisible ) { this.buttonView.isVisible = false; @@ -115,8 +114,9 @@ export default class BlockToolbar extends Plugin { } /** - * Creates toolbar components based on given configuration. - * This needs to be done when all plugins are ready. + * Fill the toolbar with its items based on the configuration. + * + * **Note:** This needs to be done after all plugins are ready. * * @inheritDoc */ @@ -133,7 +133,7 @@ export default class BlockToolbar extends Plugin { } /** - * Creates toolbar view. + * Creates the {@link #toolbarView}. * * @private * @returns {module:ui/toolbar/toolbarview~ToolbarView} @@ -154,7 +154,7 @@ export default class BlockToolbar extends Plugin { } /** - * Creates panel view. + * Creates the {@link #panelView}. * * @private * @returns {module:ui/panel/balloon/balloonpanelview~BalloonPanelView} @@ -178,7 +178,7 @@ export default class BlockToolbar extends Plugin { } /** - * Creates button view. + * Creates the {@link #buttonView}. * * @private * @returns {module:ui/toolbar/block/view/blockbuttonview~BlockButtonView} @@ -187,15 +187,17 @@ export default class BlockToolbar extends Plugin { const editor = this.editor; const buttonView = new BlockButtonView( editor.locale ); - buttonView.label = editor.t( 'Edit block' ); - buttonView.icon = iconPilcrow; - buttonView.withText = false; + buttonView.set( { + label: editor.t( 'Edit block' ), + icon: iconPilcrow, + withText: false + } ); - // Bind panelView to buttonView. + // Bind the panelView observable properties to the buttonView. buttonView.bind( 'isOn' ).to( this.panelView, 'isVisible' ); buttonView.bind( 'tooltip' ).to( this.panelView, 'isVisible', isVisible => !isVisible ); - // Toggle panelView on buttonView#execute. + // Toggle the panelView upon buttonView#execute. this.listenTo( buttonView, 'execute', () => { if ( !this.panelView.isVisible ) { this._showPanel(); @@ -211,7 +213,7 @@ export default class BlockToolbar extends Plugin { } /** - * Starts displaying button next to allowed elements. + * Starts displaying the button next to allowed elements. * * @private */ @@ -269,7 +271,7 @@ export default class BlockToolbar extends Plugin { } /** - * Attaches #buttonView to the target block of content. + * Attaches the {@link #buttonView} to the target block of content. * * @protected * @param {HTMLElement} targetElement Target element. @@ -301,8 +303,8 @@ export default class BlockToolbar extends Plugin { } /** - * Shows toolbar attached to the block button. - * When toolbar is already opened then just repositions it. + * Shows the {@link #toolbarView} attached to the {@link #buttonView}. + * If the toolbar is already visible, then it simply repositions it. * * @private */ @@ -320,7 +322,7 @@ export default class BlockToolbar extends Plugin { } /** - * Hides toolbar. + * Hides the {@link #toolbarView}. * * @private * @param {Boolean} [focusEditable=false] When `true` then editable will be focused after hiding panel. @@ -335,7 +337,7 @@ export default class BlockToolbar extends Plugin { } /** - * Block toolbar configuration. Used by the {@link module:ui/toolbar/block/blocktoolbar~BlockToolbar} + * The block toolbar configuration. Used by the {@link module:ui/toolbar/block/blocktoolbar~BlockToolbar} * feature. * * const config = { @@ -348,7 +350,7 @@ export default class BlockToolbar extends Plugin { * blockToolbar: [ 'paragraph', 'heading1', 'heading2', '|', 'bulletedList', 'numberedList' ] * }; * - * Read also about configuring the main editor toolbar in {@link module:core/editor/editorconfig~EditorConfig#toolbar}. + * Read more about configuring the main editor toolbar in {@link module:core/editor/editorconfig~EditorConfig#toolbar}. * * @member {Array.|Object} module:core/editor/editorconfig~EditorConfig#blockToolbar */ From 49b3538379256aa41780ccd524987a288fa8aa05 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 28 May 2018 16:09:08 +0200 Subject: [PATCH 53/57] Docs, Tests: Improved comments and manual test descriptions. --- src/toolbar/block/view/blockbuttonview.js | 2 +- tests/manual/blocktoolbar/blocktoolbar.html | 2 +- tests/manual/blocktoolbar/blocktoolbar.md | 15 +++++++-------- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/toolbar/block/view/blockbuttonview.js b/src/toolbar/block/view/blockbuttonview.js index 97994030..4c34c1c1 100644 --- a/src/toolbar/block/view/blockbuttonview.js +++ b/src/toolbar/block/view/blockbuttonview.js @@ -15,7 +15,7 @@ const toPx = toUnit( 'px' ); /** * The block button view class. * - * This view represents button that will be attached next to block element where the selection is placed. + * This view represents a button attached next to block element where the selection is anchored. * * See {@link module:ui/toolbar/block/blocktoolbar~BlockToolbar}. * diff --git a/tests/manual/blocktoolbar/blocktoolbar.html b/tests/manual/blocktoolbar/blocktoolbar.html index 1a5e65e4..596cebac 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.html +++ b/tests/manual/blocktoolbar/blocktoolbar.html @@ -50,7 +50,7 @@

Confidence

} .wrapper { - padding: 0 20px; + padding: 50px 20px; } .ck-block-toolbar-button { diff --git a/tests/manual/blocktoolbar/blocktoolbar.md b/tests/manual/blocktoolbar/blocktoolbar.md index fb282e5b..97b8c509 100644 --- a/tests/manual/blocktoolbar/blocktoolbar.md +++ b/tests/manual/blocktoolbar/blocktoolbar.md @@ -1,20 +1,19 @@ ## Block toolbar demo -1. Check if button appears next to all block elements except image (no toolbar item available). -2. Change format of one of the block elements, panel attached to button should hide after that. -3. Put selection in the one of the last blocks, click on button to display panel then start resize browser window -and observe if button and panel are properly repositioned. +1. Check if the button appears next to all block elements except the image (no toolbar item available). +2. Change the format of one of the block elements, panel attached to button should hide after that. +3. Put the selection in the one of the last blocks, click the button to display the panel then start resizing the browser window and observe if the button and panel are properly repositioned. ### External changes ## Typing 1. Click `Start external typing` -2. Put selection to the first paragraph (`Like all the great things...`) and click the button to open panel (be quick). -3. Check if button and panel are repositioned correctly. +2. Put the selection in the first paragraph (`Like all the great things...`) and click the button to open the panel (be quick). +3. Check if the button and the panel are repositioned correctly. ## Removing 1. Click `Start external deleting` -2. Put selection to the first paragraph (`Like all the great things...`) and click the button to open panel (be quick). -3. Check if button and panel are reattached. +2. Put the selection in the first paragraph (`Like all the great things...`) and click the button to open the panel (be quick). +3. Check if the button and the panel are reattached. From b5ddbfdf040ae6e47ec4aef8c16b352bdca25a51 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 28 May 2018 16:19:11 +0200 Subject: [PATCH 54/57] Tests: Improved assertions in the BlockToolbar tests. --- tests/toolbar/block/blocktoolbar.js | 56 ++++++++++----------- tests/toolbar/block/view/blockbuttonview.js | 4 +- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/tests/toolbar/block/blocktoolbar.js b/tests/toolbar/block/blocktoolbar.js index cb1b0501..4c88d6da 100644 --- a/tests/toolbar/block/blocktoolbar.js +++ b/tests/toolbar/block/blocktoolbar.js @@ -70,11 +70,11 @@ describe( 'BlockToolbar', () => { } ); it( 'should add panelView to ui.focusTracker', () => { - expect( editor.ui.focusTracker.isFocused ).to.false; + expect( editor.ui.focusTracker.isFocused ).to.be.false; blockToolbar.panelView.element.dispatchEvent( new Event( 'focus' ) ); - expect( editor.ui.focusTracker.isFocused ).to.true; + expect( editor.ui.focusTracker.isFocused ).to.be.true; } ); it( 'should close panelView after `Esc` press and focus view document', () => { @@ -88,7 +88,7 @@ describe( 'BlockToolbar', () => { stopPropagation: () => {} } ); - expect( blockToolbar.panelView.isVisible ).to.false; + expect( blockToolbar.panelView.isVisible ).to.be.false; sinon.assert.calledOnce( spy ); } ); @@ -99,7 +99,7 @@ describe( 'BlockToolbar', () => { blockToolbar.panelView.isVisible = true; document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) ); - expect( blockToolbar.panelView.isVisible ).to.false; + expect( blockToolbar.panelView.isVisible ).to.be.false; sinon.assert.notCalled( spy ); } ); @@ -107,7 +107,7 @@ describe( 'BlockToolbar', () => { blockToolbar.panelView.isVisible = true; blockToolbar.panelView.element.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) ); - expect( blockToolbar.panelView.isVisible ).to.true; + expect( blockToolbar.panelView.isVisible ).to.be.true; } ); } ); @@ -117,7 +117,7 @@ describe( 'BlockToolbar', () => { } ); it( 'should add additional class to toolbar element', () => { - expect( blockToolbar.toolbarView.element.classList.contains( 'ck-toolbar_floating' ) ).to.true; + expect( blockToolbar.toolbarView.element.classList.contains( 'ck-toolbar_floating' ) ).to.be.true; } ); it( 'should be added to panelView#content collection', () => { @@ -131,11 +131,11 @@ describe( 'BlockToolbar', () => { it( 'should hide panel after clicking on the button from toolbar', () => { blockToolbar.buttonView.fire( 'execute' ); - expect( blockToolbar.panelView.isVisible ).to.true; + expect( blockToolbar.panelView.isVisible ).to.be.true; blockToolbar.toolbarView.items.get( 0 ).fire( 'execute' ); - expect( blockToolbar.panelView.isVisible ).to.false; + expect( blockToolbar.panelView.isVisible ).to.be.false; } ); } ); @@ -149,22 +149,22 @@ describe( 'BlockToolbar', () => { } ); it( 'should add buttonView to ui.focusTracker', () => { - expect( editor.ui.focusTracker.isFocused ).to.false; + expect( editor.ui.focusTracker.isFocused ).to.be.false; blockToolbar.buttonView.element.dispatchEvent( new Event( 'focus' ) ); - expect( editor.ui.focusTracker.isFocused ).to.true; + expect( editor.ui.focusTracker.isFocused ).to.be.true; } ); it( 'should pin panelView to the button and focus first item in toolbar on #execute event', () => { - expect( blockToolbar.panelView.isVisible ).to.false; + expect( blockToolbar.panelView.isVisible ).to.be.false; const pinSpy = testUtils.sinon.spy( blockToolbar.panelView, 'pin' ); const focusSpy = testUtils.sinon.spy( blockToolbar.toolbarView.items.get( 0 ), 'focus' ); blockToolbar.buttonView.fire( 'execute' ); - expect( blockToolbar.panelView.isVisible ).to.true; + expect( blockToolbar.panelView.isVisible ).to.be.true; sinon.assert.calledWith( pinSpy, { target: blockToolbar.buttonView.element, limiter: editor.ui.view.editableElement @@ -178,28 +178,28 @@ describe( 'BlockToolbar', () => { blockToolbar.buttonView.fire( 'execute' ); - expect( blockToolbar.panelView.isVisible ).to.false; + expect( blockToolbar.panelView.isVisible ).to.be.false; sinon.assert.calledOnce( spy ); } ); it( 'should bind #isOn to panelView#isVisible', () => { blockToolbar.panelView.isVisible = false; - expect( blockToolbar.buttonView.isOn ).to.false; + expect( blockToolbar.buttonView.isOn ).to.be.false; blockToolbar.panelView.isVisible = true; - expect( blockToolbar.buttonView.isOn ).to.true; + expect( blockToolbar.buttonView.isOn ).to.be.true; } ); it( 'should hide button tooltip when panelView is opened', () => { blockToolbar.panelView.isVisible = false; - expect( blockToolbar.buttonView.tooltip ).to.true; + expect( blockToolbar.buttonView.tooltip ).to.be.true; blockToolbar.panelView.isVisible = true; - expect( blockToolbar.buttonView.tooltip ).to.false; + expect( blockToolbar.buttonView.tooltip ).to.be.false; } ); } ); } ); @@ -211,25 +211,25 @@ describe( 'BlockToolbar', () => { setData( editor.model, 'foo[]bar' ); - expect( blockToolbar.buttonView.isVisible ).to.true; + expect( blockToolbar.buttonView.isVisible ).to.be.true; } ); it( 'should display button when the first selected block is an object', () => { setData( editor.model, '[foo]' ); - expect( blockToolbar.buttonView.isVisible ).to.true; + expect( blockToolbar.buttonView.isVisible ).to.be.true; } ); it( 'should display button when the selection is inside the object', () => { setData( editor.model, 'f[]oo' ); - expect( blockToolbar.buttonView.isVisible ).to.true; + expect( blockToolbar.buttonView.isVisible ).to.be.true; } ); it( 'should not display button when the selection is placed in a root element', () => { setData( editor.model, 'foo[]bar' ); - expect( blockToolbar.buttonView.isVisible ).to.false; + expect( blockToolbar.buttonView.isVisible ).to.be.false; } ); it( 'should not display button when all toolbar items are disabled for the selected element', () => { @@ -245,7 +245,7 @@ describe( 'BlockToolbar', () => { setData( editor.model, '[]' ); - expect( blockToolbar.buttonView.isVisible ).to.false; + expect( blockToolbar.buttonView.isVisible ).to.be.false; element.remove(); @@ -358,7 +358,7 @@ describe( 'BlockToolbar', () => { editor.model.document.selection.fire( 'change:range', { directChange: true } ); - expect( blockToolbar.panelView.isVisible ).to.false; + expect( blockToolbar.panelView.isVisible ).to.be.false; } ); it( 'should not hide opened panel on a selection not direct change', () => { @@ -366,7 +366,7 @@ describe( 'BlockToolbar', () => { editor.model.document.selection.fire( 'change:range', { directChange: false } ); - expect( blockToolbar.panelView.isVisible ).to.true; + expect( blockToolbar.panelView.isVisible ).to.be.true; } ); it( 'should hide UI when editor switch to readonly when panel is not visible', () => { @@ -377,8 +377,8 @@ describe( 'BlockToolbar', () => { editor.isReadOnly = true; - expect( blockToolbar.buttonView.isVisible ).to.false; - expect( blockToolbar.panelView.isVisible ).to.false; + expect( blockToolbar.buttonView.isVisible ).to.be.false; + expect( blockToolbar.panelView.isVisible ).to.be.false; } ); it( 'should not hide button when editor switch to readonly when panel is visible', () => { @@ -389,8 +389,8 @@ describe( 'BlockToolbar', () => { editor.isReadOnly = true; - expect( blockToolbar.buttonView.isVisible ).to.true; - expect( blockToolbar.panelView.isVisible ).to.true; + expect( blockToolbar.buttonView.isVisible ).to.be.true; + expect( blockToolbar.panelView.isVisible ).to.be.true; } ); it( 'should update button position on browser resize only when button is visible', () => { diff --git a/tests/toolbar/block/view/blockbuttonview.js b/tests/toolbar/block/view/blockbuttonview.js index e7f65217..09366684 100644 --- a/tests/toolbar/block/view/blockbuttonview.js +++ b/tests/toolbar/block/view/blockbuttonview.js @@ -14,11 +14,11 @@ describe( 'BlockButtonView', () => { } ); it( 'should be not visible on init', () => { - expect( view.isVisible ).to.false; + expect( view.isVisible ).to.be.false; } ); it( 'should create element from template', () => { - expect( view.element.classList.contains( 'ck-block-toolbar-button' ) ).to.true; + expect( view.element.classList.contains( 'ck-block-toolbar-button' ) ).to.be.true; } ); describe( 'DOM binding', () => { From a8e7cf420d8e92e05ef37c666931e761cc10aa66 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 28 May 2018 16:27:28 +0200 Subject: [PATCH 55/57] Tests: Improved ButtonToolbar unit test descriptions. --- tests/toolbar/block/blocktoolbar.js | 72 ++++++++++++++--------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/tests/toolbar/block/blocktoolbar.js b/tests/toolbar/block/blocktoolbar.js index 4c88d6da..73e1ef4b 100644 --- a/tests/toolbar/block/blocktoolbar.js +++ b/tests/toolbar/block/blocktoolbar.js @@ -51,25 +51,25 @@ describe( 'BlockToolbar', () => { expect( BlockToolbar.pluginName ).to.equal( 'BlockToolbar' ); } ); - it( 'should register click observer', () => { + it( 'should register a click observer', () => { expect( editor.editing.view.getObserver( ClickObserver ) ).to.be.instanceOf( ClickObserver ); } ); describe( 'child views', () => { describe( 'panelView', () => { - it( 'should create view instance', () => { + it( 'should create a view instance', () => { expect( blockToolbar.panelView ).to.instanceof( BalloonPanelView ); } ); - it( 'should have additional class name', () => { + it( 'should have an additional class name', () => { expect( blockToolbar.panelView.className ).to.equal( 'ck-toolbar-container' ); } ); - it( 'should be added to ui.view.body collection', () => { + it( 'should be added to the ui.view.body collection', () => { expect( Array.from( editor.ui.view.body ) ).to.include( blockToolbar.panelView ); } ); - it( 'should add panelView to ui.focusTracker', () => { + it( 'should add the #panelView to ui.focusTracker', () => { expect( editor.ui.focusTracker.isFocused ).to.be.false; blockToolbar.panelView.element.dispatchEvent( new Event( 'focus' ) ); @@ -77,7 +77,7 @@ describe( 'BlockToolbar', () => { expect( editor.ui.focusTracker.isFocused ).to.be.true; } ); - it( 'should close panelView after `Esc` press and focus view document', () => { + it( 'should close the #panelView after `Esc` is pressed and focus view document', () => { const spy = testUtils.sinon.spy( editor.editing.view, 'focus' ); blockToolbar.panelView.isVisible = true; @@ -92,7 +92,7 @@ describe( 'BlockToolbar', () => { sinon.assert.calledOnce( spy ); } ); - it( 'should close panelView on click outside the panel and not focus view document', () => { + it( 'should close the #panelView upon click outside the panel and not focus view document', () => { const spy = testUtils.sinon.spy(); editor.editing.view.on( 'focus', spy ); @@ -103,7 +103,7 @@ describe( 'BlockToolbar', () => { sinon.assert.notCalled( spy ); } ); - it( 'should not close panelView on click on panel element', () => { + it( 'should not close the #panelView upon click on panel element', () => { blockToolbar.panelView.isVisible = true; blockToolbar.panelView.element.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) ); @@ -112,23 +112,23 @@ describe( 'BlockToolbar', () => { } ); describe( 'toolbarView', () => { - it( 'should create view instance', () => { + it( 'should create the view instance', () => { expect( blockToolbar.toolbarView ).to.instanceof( ToolbarView ); } ); - it( 'should add additional class to toolbar element', () => { + it( 'should add an additional class to toolbar element', () => { expect( blockToolbar.toolbarView.element.classList.contains( 'ck-toolbar_floating' ) ).to.be.true; } ); - it( 'should be added to panelView#content collection', () => { + it( 'should be added to the panelView#content collection', () => { expect( Array.from( blockToolbar.panelView.content ) ).to.include( blockToolbar.toolbarView ); } ); - it( 'should initialize toolbar items based on Editor#blockToolbar config', () => { + it( 'should initialize the toolbar items based on Editor#blockToolbar config', () => { expect( Array.from( blockToolbar.toolbarView.items ) ).to.length( 4 ); } ); - it( 'should hide panel after clicking on the button from toolbar', () => { + it( 'should hide the panel after clicking on the button from toolbar', () => { blockToolbar.buttonView.fire( 'execute' ); expect( blockToolbar.panelView.isVisible ).to.be.true; @@ -140,15 +140,15 @@ describe( 'BlockToolbar', () => { } ); describe( 'buttonView', () => { - it( 'should create view instance', () => { + it( 'should create a view instance', () => { expect( blockToolbar.buttonView ).to.instanceof( BlockButtonView ); } ); - it( 'should be added to editor ui.view.body collection', () => { + it( 'should be added to the editor ui.view.body collection', () => { expect( Array.from( editor.ui.view.body ) ).to.include( blockToolbar.buttonView ); } ); - it( 'should add buttonView to ui.focusTracker', () => { + it( 'should add the #buttonView to the ui.focusTracker', () => { expect( editor.ui.focusTracker.isFocused ).to.be.false; blockToolbar.buttonView.element.dispatchEvent( new Event( 'focus' ) ); @@ -156,7 +156,7 @@ describe( 'BlockToolbar', () => { expect( editor.ui.focusTracker.isFocused ).to.be.true; } ); - it( 'should pin panelView to the button and focus first item in toolbar on #execute event', () => { + it( 'should pin the #panelView to the button and focus first item in toolbar on #execute event', () => { expect( blockToolbar.panelView.isVisible ).to.be.false; const pinSpy = testUtils.sinon.spy( blockToolbar.panelView, 'pin' ); @@ -172,7 +172,7 @@ describe( 'BlockToolbar', () => { sinon.assert.calledOnce( focusSpy ); } ); - it( 'should hide panelView and focus editable on #execute event when panel was visible', () => { + it( 'should hide the #panelView and focus the editable on #execute event when panel was visible', () => { blockToolbar.panelView.isVisible = true; const spy = testUtils.sinon.spy( editor.editing.view, 'focus' ); @@ -192,7 +192,7 @@ describe( 'BlockToolbar', () => { expect( blockToolbar.buttonView.isOn ).to.be.true; } ); - it( 'should hide button tooltip when panelView is opened', () => { + it( 'should hide the #button tooltip when the #panelView is open', () => { blockToolbar.panelView.isVisible = false; expect( blockToolbar.buttonView.tooltip ).to.be.true; @@ -205,7 +205,7 @@ describe( 'BlockToolbar', () => { } ); describe( 'allowed elements', () => { - it( 'should display button when the first selected block is a block element', () => { + it( 'should display the button when the first selected block is a block element', () => { editor.model.schema.register( 'foo', { inheritAllFrom: '$block' } ); editor.conversion.elementToElement( { model: 'foo', view: 'foo' } ); @@ -214,25 +214,25 @@ describe( 'BlockToolbar', () => { expect( blockToolbar.buttonView.isVisible ).to.be.true; } ); - it( 'should display button when the first selected block is an object', () => { + it( 'should display the button when the first selected block is an object', () => { setData( editor.model, '[foo]' ); expect( blockToolbar.buttonView.isVisible ).to.be.true; } ); - it( 'should display button when the selection is inside the object', () => { + it( 'should display the button when the selection is inside the object', () => { setData( editor.model, 'f[]oo' ); expect( blockToolbar.buttonView.isVisible ).to.be.true; } ); - it( 'should not display button when the selection is placed in a root element', () => { + it( 'should not display the button when the selection is placed in a root element', () => { setData( editor.model, 'foo[]bar' ); expect( blockToolbar.buttonView.isVisible ).to.be.false; } ); - it( 'should not display button when all toolbar items are disabled for the selected element', () => { + it( 'should not display the button when all toolbar items are disabled for the selected element', () => { const element = document.createElement( 'div' ); document.body.appendChild( element ); @@ -254,9 +254,9 @@ describe( 'BlockToolbar', () => { } ); } ); - describe( 'attaching button to the content', () => { - it( 'should attach right side of the button to the left side of the editable and center with the first line ' + - 'of selected block #1', () => { + describe( 'attaching the button to the content', () => { + it( 'should attach the right side of the button to the left side of the editable and center with the first line ' + + 'of the selected block #1', () => { setData( editor.model, 'foo[]bar' ); const target = editor.ui.view.editableElement.querySelector( 'p' ); @@ -289,8 +289,8 @@ describe( 'BlockToolbar', () => { expect( blockToolbar.buttonView.left ).to.equal( 100 ); } ); - it( 'should attach right side of the button to the left side of the editable and center with the first line ' + - 'of selected block #2', () => { + it( 'should attach the right side of the button to the left side of the editable and center with the first line ' + + 'of the selected block #2', () => { setData( editor.model, 'foo[]bar' ); const target = editor.ui.view.editableElement.querySelector( 'p' ); @@ -324,7 +324,7 @@ describe( 'BlockToolbar', () => { expect( blockToolbar.buttonView.left ).to.equal( 100 ); } ); - it( 'should reposition panelView when is opened on view#render', () => { + it( 'should reposition the #panelView when open on view#render', () => { blockToolbar.panelView.isVisible = false; const spy = testUtils.sinon.spy( blockToolbar.panelView, 'pin' ); @@ -343,7 +343,7 @@ describe( 'BlockToolbar', () => { } ); } ); - it( 'should not reset toolbar focus on view#render', () => { + it( 'should not reset the toolbar focus on view#render', () => { blockToolbar.panelView.isVisible = true; const spy = testUtils.sinon.spy( blockToolbar.toolbarView, 'focus' ); @@ -353,7 +353,7 @@ describe( 'BlockToolbar', () => { sinon.assert.notCalled( spy ); } ); - it( 'should hide opened panel on a selection direct change', () => { + it( 'should hide the open panel on a direct selection change', () => { blockToolbar.panelView.isVisible = true; editor.model.document.selection.fire( 'change:range', { directChange: true } ); @@ -361,7 +361,7 @@ describe( 'BlockToolbar', () => { expect( blockToolbar.panelView.isVisible ).to.be.false; } ); - it( 'should not hide opened panel on a selection not direct change', () => { + it( 'should not hide the open panel on a indirect selection change', () => { blockToolbar.panelView.isVisible = true; editor.model.document.selection.fire( 'change:range', { directChange: false } ); @@ -369,7 +369,7 @@ describe( 'BlockToolbar', () => { expect( blockToolbar.panelView.isVisible ).to.be.true; } ); - it( 'should hide UI when editor switch to readonly when panel is not visible', () => { + it( 'should hide the UI when editor switches to readonly when the panel is not visible', () => { setData( editor.model, 'foo[]bar' ); blockToolbar.buttonView.isVisible = true; @@ -381,7 +381,7 @@ describe( 'BlockToolbar', () => { expect( blockToolbar.panelView.isVisible ).to.be.false; } ); - it( 'should not hide button when editor switch to readonly when panel is visible', () => { + it( 'should not hide button when the editor switches to readonly when the panel is visible', () => { setData( editor.model, 'foo[]bar' ); blockToolbar.buttonView.isVisible = true; @@ -393,7 +393,7 @@ describe( 'BlockToolbar', () => { expect( blockToolbar.panelView.isVisible ).to.be.true; } ); - it( 'should update button position on browser resize only when button is visible', () => { + it( 'should update the button position on browser resize only when the button is visible', () => { const spy = testUtils.sinon.spy( blockToolbar, '_attachButtonToElement' ); setData( editor.model, '[]bar' ); From 54ef26f507f83cfe90f4ba555d36e9f621ac59d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Tue, 29 May 2018 09:20:05 +0200 Subject: [PATCH 56/57] Moved BlockButtonView from the view directory. --- src/toolbar/block/{view => }/blockbuttonview.js | 6 +++--- src/toolbar/block/blocktoolbar.js | 6 +++--- tests/toolbar/block/{view => }/blockbuttonview.js | 2 +- tests/toolbar/block/blocktoolbar.js | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) rename src/toolbar/block/{view => }/blockbuttonview.js (86%) rename tests/toolbar/block/{view => }/blockbuttonview.js (91%) diff --git a/src/toolbar/block/view/blockbuttonview.js b/src/toolbar/block/blockbuttonview.js similarity index 86% rename from src/toolbar/block/view/blockbuttonview.js rename to src/toolbar/block/blockbuttonview.js index 4c34c1c1..dae6fe73 100644 --- a/src/toolbar/block/view/blockbuttonview.js +++ b/src/toolbar/block/blockbuttonview.js @@ -3,12 +3,12 @@ */ /** - * @module ui/toolbar/block/view/blockbuttonview + * @module ui/toolbar/block/blockbuttonview */ -import ButtonView from '../../../button/buttonview'; +import ButtonView from '../../button/buttonview'; import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit'; -import '../../../../theme/components/toolbar/blocktoolbar.css'; +import '../../../theme/components/toolbar/blocktoolbar.css'; const toPx = toUnit( 'px' ); diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index 5058de7b..a4cb7107 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -10,7 +10,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import BlockButtonView from './view/blockbuttonview'; +import BlockButtonView from './blockbuttonview'; import BalloonPanelView from '../../panel/balloon/balloonpanelview'; import ToolbarView from '../toolbarview'; @@ -88,7 +88,7 @@ export default class BlockToolbar extends Plugin { /** * The button view, that opens the {@link #toolbarView}. * - * @type {module:ui/toolbar/block/view/blockbuttonview~BlockButtonView} + * @type {module:ui/toolbar/block/blockbuttonview~BlockButtonView} */ this.buttonView = this._createButtonView(); @@ -181,7 +181,7 @@ export default class BlockToolbar extends Plugin { * Creates the {@link #buttonView}. * * @private - * @returns {module:ui/toolbar/block/view/blockbuttonview~BlockButtonView} + * @returns {module:ui/toolbar/block/blockbuttonview~BlockButtonView} */ _createButtonView() { const editor = this.editor; diff --git a/tests/toolbar/block/view/blockbuttonview.js b/tests/toolbar/block/blockbuttonview.js similarity index 91% rename from tests/toolbar/block/view/blockbuttonview.js rename to tests/toolbar/block/blockbuttonview.js index 09366684..64940715 100644 --- a/tests/toolbar/block/view/blockbuttonview.js +++ b/tests/toolbar/block/blockbuttonview.js @@ -2,7 +2,7 @@ * Copyright (c) 2016 - 2017, CKSource - Frederico Knabben. All rights reserved. */ -import BlockButtonView from '../../../../src/toolbar/block/view/blockbuttonview'; +import BlockButtonView from '../../../src/toolbar/block/blockbuttonview'; describe( 'BlockButtonView', () => { let view; diff --git a/tests/toolbar/block/blocktoolbar.js b/tests/toolbar/block/blocktoolbar.js index 73e1ef4b..c4a5e644 100644 --- a/tests/toolbar/block/blocktoolbar.js +++ b/tests/toolbar/block/blocktoolbar.js @@ -10,7 +10,7 @@ import ClickObserver from '@ckeditor/ckeditor5-engine/src/view/observer/clickobs import BlockToolbar from '../../../src/toolbar/block/blocktoolbar'; import ToolbarView from '../../../src/toolbar/toolbarview'; import BalloonPanelView from '../../../src/panel/balloon/balloonpanelview'; -import BlockButtonView from './../../../src/toolbar/block/view/blockbuttonview'; +import BlockButtonView from '../../../src/toolbar/block/blockbuttonview'; import Heading from '@ckeditor/ckeditor5-heading/src/heading'; import HeadingButtonsUI from '@ckeditor/ckeditor5-heading/src/headingbuttonsui'; From 0e87365250fb58f1c50ba4ae091cd992028a4154 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 30 May 2018 12:58:25 +0200 Subject: [PATCH 57/57] Moved the pilcrow icon to the ckeditor5-core. --- src/toolbar/block/blocktoolbar.js | 2 +- theme/icons/pilcrow.svg | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 theme/icons/pilcrow.svg diff --git a/src/toolbar/block/blocktoolbar.js b/src/toolbar/block/blocktoolbar.js index a4cb7107..e11e6d28 100644 --- a/src/toolbar/block/blocktoolbar.js +++ b/src/toolbar/block/blocktoolbar.js @@ -20,7 +20,7 @@ import clickOutsideHandler from '../../bindings/clickoutsidehandler'; import { getOptimalPosition } from '@ckeditor/ckeditor5-utils/src/dom/position'; import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect'; -import iconPilcrow from '../../../theme/icons/pilcrow.svg'; +import iconPilcrow from '@ckeditor/ckeditor5-core/theme/icons/pilcrow.svg'; /** * The block toolbar plugin. diff --git a/theme/icons/pilcrow.svg b/theme/icons/pilcrow.svg deleted file mode 100644 index 359434b7..00000000 --- a/theme/icons/pilcrow.svg +++ /dev/null @@ -1 +0,0 @@ -