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

@ckeditor/ckeditor5-heading

Package Overview
Dependencies
Maintainers
1
Versions
708
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ckeditor/ckeditor5-heading - npm Package Compare versions

Comparing version 0.8.0 to 0.9.0

lang/translations/bg.po

27

CHANGELOG.md
Changelog
=========
## [0.9.0](https://github.com/ckeditor/ckeditor5-heading/compare/v0.8.0...v0.9.0) (2017-04-05)
### Bug fixes
* Changed the default heading drop-down title to a more meaningful one. Closes [#68](https://github.com/ckeditor/ckeditor5-heading/issues/68). Closes [#62](https://github.com/ckeditor/ckeditor5-heading/issues/62). ([1c16e96](https://github.com/ckeditor/ckeditor5-heading/commit/1c16e96)) and ([e58dadc](https://github.com/ckeditor/ckeditor5-heading/commit/e58dadc))
* Drop-down should be inactive when none of the commands can be applied to the current selection. Closes [#66](https://github.com/ckeditor/ckeditor5-heading/issues/66). ([0ebd5cd](https://github.com/ckeditor/ckeditor5-heading/commit/0ebd5cd))
### Features
* Active heading is marked in the drop-down list. Closes [#26](https://github.com/ckeditor/ckeditor5-heading/issues/26). ([39ba14b](https://github.com/ckeditor/ckeditor5-heading/commit/39ba14b))
* Enabled the tooltip for the 'headings' component in editor.ui#componentFactory. Closes [#55](https://github.com/ckeditor/ckeditor5-heading/issues/55). ([794e6df](https://github.com/ckeditor/ckeditor5-heading/commit/794e6df))
* Named existing plugin(s). ([7d512cd](https://github.com/ckeditor/ckeditor5-heading/commit/7d512cd))
* Split "heading" command into independent commands. Closes [#53](https://github.com/ckeditor/ckeditor5-heading/issues/53). Closes [#56](https://github.com/ckeditor/ckeditor5-heading/issues/56). Closes [#52](https://github.com/ckeditor/ckeditor5-heading/issues/52). ([7a8f6f0](https://github.com/ckeditor/ckeditor5-heading/commit/7a8f6f0))
* Styled items in the headings toolbar dropdown. Closes [#38](https://github.com/ckeditor/ckeditor5-heading/issues/38). ([0365333](https://github.com/ckeditor/ckeditor5-heading/commit/0365333))
### Other changes
* Introduced consistent height and spacing among headings dropdown items. Closes [#63](https://github.com/ckeditor/ckeditor5-heading/issues/63). ([68d93ff](https://github.com/ckeditor/ckeditor5-heading/commit/68d93ff))
* Updated translations. ([fc95eee](https://github.com/ckeditor/ckeditor5-heading/commit/fc95eee))
### BREAKING CHANGES
* The "heading" command is no longer available. Replaced by "heading1", "heading2", "heading3" and "paragraph".
* `Heading` plugin requires `Paragraph` to work properly (`ParagraphCommand` registered as "paragraph" in `editor.commands`).
* `config.heading.options` format has changed. The valid `HeadingOption` syntax is now `{ modelElement: 'heading1', viewElement: 'h1', title: 'Heading 1' }`.
## [0.8.0](https://github.com/ckeditor/ckeditor5-heading/compare/v0.7.0...v0.8.0) (2017-03-06)

@@ -5,0 +32,0 @@

2

lang/contexts.json
{
"Paragraph": "Drop-down option label for the paragraph format.",
"Heading": "Tooltip for the heading drop-down.",
"Choose heading": "Default label for the heading drop-down.",
"Heading 1": "Drop-down option label for the heading level 1 format.",

@@ -4,0 +6,0 @@ "Heading 2": "Drop-down option label for the heading level 2 format.",

21

package.json
{
"name": "@ckeditor/ckeditor5-heading",
"version": "0.8.0",
"version": "0.9.0",
"description": "Headings feature for CKEditor 5.",
"keywords": [],
"dependencies": {
"@ckeditor/ckeditor5-core": "^0.7.0",
"@ckeditor/ckeditor5-ui": "^0.7.1",
"@ckeditor/ckeditor5-utils": "^0.8.0",
"@ckeditor/ckeditor5-engine": "^0.8.0",
"@ckeditor/ckeditor5-paragraph": "^0.6.1"
"@ckeditor/ckeditor5-core": "^0.8.0",
"@ckeditor/ckeditor5-ui": "^0.8.0",
"@ckeditor/ckeditor5-utils": "^0.9.0",
"@ckeditor/ckeditor5-engine": "^0.9.0",
"@ckeditor/ckeditor5-paragraph": "^0.7.0",
"@ckeditor/ckeditor5-theme-lark": "^0.7.0"
},
"devDependencies": {
"@ckeditor/ckeditor5-dev-lint": "^2.0.2",
"@ckeditor/ckeditor5-enter": "^0.8.0",
"@ckeditor/ckeditor5-editor-classic": "^0.7.1",
"@ckeditor/ckeditor5-typing": "^0.8.0",
"@ckeditor/ckeditor5-undo": "^0.7.1",
"@ckeditor/ckeditor5-enter": "^0.9.0",
"@ckeditor/ckeditor5-editor-classic": "^0.7.2",
"@ckeditor/ckeditor5-typing": "^0.9.0",
"@ckeditor/ckeditor5-undo": "^0.8.0",
"gulp": "^3.9.0",

@@ -20,0 +21,0 @@ "guppy-pre-commit": "^0.4.0"

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

import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import HeadingEngine from './headingengine';
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import Model from '@ckeditor/ckeditor5-ui/src/model';
import createListDropdown from '@ckeditor/ckeditor5-ui/src/dropdown/list/createlistdropdown';
import Collection from '@ckeditor/ckeditor5-utils/src/collection';
import Template from '@ckeditor/ckeditor5-ui/src/template';
import '../theme/theme.scss';
/**

@@ -31,3 +32,3 @@ * The headings feature. It introduces the `headings` drop-down list and the `heading` command which allow

static get requires() {
return [ HeadingEngine ];
return [ Paragraph, HeadingEngine ];
}

@@ -38,13 +39,32 @@

*/
static get pluginName() {
return 'heading/heading';
}
/**
* @inheritDoc
*/
init() {
const editor = this.editor;
const command = editor.commands.get( 'heading' );
const options = command.options;
const collection = new Collection();
const dropdownItems = new Collection();
const options = this._getLocalizedOptions();
const commands = [];
const t = editor.t;
const defaultTitle = t( 'Choose heading' );
const dropdownTooltip = t( 'Heading' );
// Add options to collection.
for ( const { id, label } of options ) {
collection.add( new Model( {
id, label
} ) );
for ( let option of options ) {
const command = editor.commands.get( option.modelElement );
const itemModel = new Model( {
commandName: option.modelElement,
label: option.title,
class: option.class
} );
itemModel.bind( 'isActive' ).to( command, 'value' );
// Add the option to the collection.
dropdownItems.add( itemModel );
commands.push( command );
}

@@ -55,9 +75,25 @@

withText: true,
items: collection
items: dropdownItems,
tooltip: dropdownTooltip
} );
// Bind dropdown model to command.
dropdownModel.bind( 'isEnabled' ).to( command, 'isEnabled' );
dropdownModel.bind( 'label' ).to( command, 'value', option => option.label );
dropdownModel.bind( 'isEnabled' ).to(
// Bind to #isEnabled of each command...
...getCommandsBindingTargets( commands, 'isEnabled' ),
// ...and set it true if any command #isEnabled is true.
( ...areEnabled ) => areEnabled.some( isEnabled => isEnabled )
);
dropdownModel.bind( 'label' ).to(
// Bind to #value of each command...
...getCommandsBindingTargets( commands, 'value' ),
// ...and chose the title of the first one which #value is true.
( ...areActive ) => {
const index = areActive.findIndex( value => value );
// If none of the commands is active, display default title.
return options[ index ] ? options[ index ].title : defaultTitle;
}
);
// Register UI component.

@@ -67,5 +103,13 @@ editor.ui.componentFactory.add( 'headings', ( locale ) => {

Template.extend( dropdown.template, {
attributes: {
class: [
'ck-heading-dropdown'
]
}
} );
// Execute command when an item from the dropdown is selected.
this.listenTo( dropdown, 'execute', ( { source: { id } } ) => {
editor.execute( 'heading', { id } );
this.listenTo( dropdown, 'execute', ( evt ) => {
editor.execute( evt.source.commandName );
editor.editing.view.focus();

@@ -77,2 +121,47 @@ } );

}
/**
* Returns heading options as defined in `config.heading.options` but processed to consider
* editor localization, i.e. to display {@link module:heading/headingcommand~HeadingOption}
* in the correct language.
*
* Note: The reason behind this method is that there's no way to use {@link module:utils/locale~Locale#t}
* when the user config is defined because the editor does not exist yet.
*
* @private
* @returns {Array.<module:heading/headingcommand~HeadingOption>}.
*/
_getLocalizedOptions() {
const editor = this.editor;
const t = editor.t;
const localizedTitles = {
Paragraph: t( 'Paragraph' ),
'Heading 1': t( 'Heading 1' ),
'Heading 2': t( 'Heading 2' ),
'Heading 3': t( 'Heading 3' )
};
return editor.config.get( 'heading.options' ).map( option => {
const title = localizedTitles[ option.title ];
if ( title && title != option.title ) {
// Clone the option to avoid altering the original `config.heading.options`.
option = Object.assign( {}, option, { title } );
}
return option;
} );
}
}
// Returns an array of binding components for
// {@link module:utils/observablemixin~Observable#bind} from a set of iterable
// commands.
//
// @private
// @param {Iterable.<module:core/command/command~Command>} commands
// @param {String} attribute
// @returns {Array.<String>}
function getCommandsBindingTargets( commands, attribute ) {
return Array.prototype.concat( ...commands.map( c => [ c, attribute ] ) );
}

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

import Range from '@ckeditor/ckeditor5-engine/src/model/range';
import Command from '@ckeditor/ckeditor5-core/src/command/command';
import RootElement from '@ckeditor/ckeditor5-engine/src/model/rootelement';
import Selection from '@ckeditor/ckeditor5-engine/src/model/selection';
import Position from '@ckeditor/ckeditor5-engine/src/model/position';
import first from '@ckeditor/ckeditor5-utils/src/first';

@@ -24,48 +27,51 @@ /**

* @param {module:core/editor/editor~Editor} editor Editor instance.
* @param {Array.<module:heading/headingcommand~HeadingOption>} options Heading options to be used by the command instance.
* @param {module:heading/headingcommand~HeadingOption} option An option to be used by the command instance.
*/
constructor( editor, options, defaultOptionId ) {
constructor( editor, option ) {
super( editor );
Object.assign( this, option );
/**
* Heading options used by this command.
* Value of the command, indicating whether it is applied in the context
* of current {@link module:engine/model/document~Document#selection selection}.
*
* @readonly
* @member {module:heading/headingcommand~HeadingOption}
* @observable
* @member {Boolean}
*/
this.options = options;
this.set( 'value', false );
// Update current value each time changes are done on document.
this.listenTo( editor.document, 'changesDone', () => {
this.refreshValue();
this.refreshState();
} );
/**
* The id of the default option among {@link #options}.
* Unique identifier of the command, also element's name in the model.
* See {@link module:heading/headingcommand~HeadingOption}.
*
* @readonly
* @private
* @member {module:heading/headingcommand~HeadingOption#id}
* @member {String} #modelElement
*/
this._defaultOptionId = defaultOptionId;
/**
* The currently selected heading option.
* Element this command creates in the view.
* See {@link module:heading/headingcommand~HeadingOption}.
*
* @readonly
* @observable
* @member {module:heading/headingcommand~HeadingOption} #value
* @member {String} #viewElement
*/
this.set( 'value', this.defaultOption );
// Update current value each time changes are done on document.
this.listenTo( editor.document, 'changesDone', () => this._updateValue() );
/**
* User-readable title of the command.
* See {@link module:heading/headingcommand~HeadingOption}.
*
* @readonly
* @member {String} #title
*/
}
/**
* The default option.
*
* @member {module:heading/headingcommand~HeadingOption} #defaultOption
*/
get defaultOption() {
// See https://github.com/ckeditor/ckeditor5/issues/98.
return this._getOptionById( this._defaultOptionId );
}
/**
* Executes command.

@@ -75,6 +81,2 @@ *

* @param {Object} [options] Options for executed command.
* @param {String} [options.id] The identifier of the heading option that should be applied. It should be one of the
* {@link module:heading/headingcommand~HeadingOption heading options} provided to the command constructor. If this parameter is not
* provided,
* the value from {@link #defaultOption defaultOption} will be used.
* @param {module:engine/model/batch~Batch} [options.batch] Batch to collect all the change steps.

@@ -84,55 +86,29 @@ * New batch will be created if this option is not set.

_doExecute( options = {} ) {
// TODO: What should happen if option is not found?
const id = options.id || this.defaultOption.id;
const doc = this.editor.document;
const selection = doc.selection;
const startPosition = selection.getFirstPosition();
const elements = [];
// Storing selection ranges and direction to fix selection after renaming. See ckeditor5-engine#367.
const ranges = [ ...selection.getRanges() ];
const isSelectionBackward = selection.isBackward;
const editor = this.editor;
const document = editor.document;
// If current option is same as new option - toggle already applied option back to default one.
const shouldRemove = ( id === this.value.id );
const shouldRemove = this.value;
// Collect elements to change option.
// This implementation may not be future proof but it's satisfactory at this stage.
if ( selection.isCollapsed ) {
const block = findTopmostBlock( startPosition );
document.enqueueChanges( () => {
const batch = options.batch || document.batch();
if ( block ) {
elements.push( block );
}
} else {
for ( let range of ranges ) {
let startBlock = findTopmostBlock( range.start );
const endBlock = findTopmostBlock( range.end, false );
elements.push( startBlock );
while ( startBlock !== endBlock ) {
startBlock = startBlock.nextSibling;
elements.push( startBlock );
}
}
}
doc.enqueueChanges( () => {
const batch = options.batch || doc.batch();
for ( let element of elements ) {
for ( let block of document.selection.getSelectedBlocks() ) {
// When removing applied option.
if ( shouldRemove ) {
if ( element.name === id ) {
batch.rename( element, this.defaultOption.id );
if ( block.is( this.modelElement ) ) {
// Apply paragraph to the selection withing that particular block only instead
// of working on the entire document selection.
const selection = new Selection();
selection.addRange( Range.createIn( block ) );
// Share the batch with the paragraph command.
editor.execute( 'paragraph', { selection, batch } );
}
}
// When applying new option.
else {
batch.rename( element, id );
else if ( !block.is( this.modelElement ) ) {
batch.rename( block, this.modelElement );
}
}
// If range's selection start/end is placed directly in renamed block - we need to restore it's position
// after renaming, because renaming puts new element there.
doc.selection.setRanges( ranges, isSelectionBackward );
} );

@@ -142,52 +118,23 @@ }

/**
* Returns the option by a given ID.
*
* @private
* @param {String} id
* @returns {module:heading/headingcommand~HeadingOption}
* Updates command's {@link #value value} based on current selection.
*/
_getOptionById( id ) {
return this.options.find( item => item.id === id ) || this.defaultOption;
refreshValue() {
const block = first( this.editor.document.selection.getSelectedBlocks() );
this.value = !!block && block.is( this.modelElement );
}
/**
* Updates command's {@link #value value} based on current selection.
*
* @private
* @inheritDoc
*/
_updateValue() {
const position = this.editor.document.selection.getFirstPosition();
const block = findTopmostBlock( position );
_checkEnabled() {
const block = first( this.editor.document.selection.getSelectedBlocks() );
if ( block ) {
this.value = this._getOptionById( block.name );
}
return !!block && this.editor.document.schema.check( {
name: this.modelElement,
inside: Position.createBefore( block )
} );
}
}
// Looks for the topmost element in the position's ancestor (up to an element in the root).
//
// NOTE: This method does not check the schema directly &mdash; it assumes that only block elements can be placed directly inside
// the root.
//
// @private
// @param {engine.model.Position} position
// @param {Boolean} [nodeAfter=true] When the position is placed inside the root element, this will determine if the element before
// or after a given position will be returned.
// @returns {engine.model.Element}
function findTopmostBlock( position, nodeAfter = true ) {
let parent = position.parent;
// If position is placed inside root - get element after/before it.
if ( parent instanceof RootElement ) {
return nodeAfter ? position.nodeAfter : position.nodeBefore;
}
while ( !( parent.parent instanceof RootElement ) ) {
parent = parent.parent;
}
return parent;
}
/**

@@ -197,5 +144,5 @@ * Heading option descriptor.

* @typedef {Object} module:heading/headingcommand~HeadingOption
* @property {String} id Option identifier. It will be used as the element's name in the model.
* @property {String} element The name of the view element that will be used to represent the model element in the view.
* @property {String} label The display name of the option.
* @property {String} modelElement Element's name in the model.
* @property {String} viewElement The name of the view element that will be used to represent the model element in the view.
* @property {String} title The user-readable title of the option.
*/

@@ -16,3 +16,3 @@ /**

const defaultOptionId = 'paragraph';
const defaultModelElement = 'paragraph';

@@ -32,8 +32,11 @@ /**

// TODO: This needs proper documentation, i.e. why paragraph entry does not need
// more properties (https://github.com/ckeditor/ckeditor5/issues/403).
// TODO: Document CSS classes as well.
editor.config.define( 'heading', {
options: [
{ id: 'paragraph', element: 'p', label: 'Paragraph' },
{ id: 'heading1', element: 'h2', label: 'Heading 1' },
{ id: 'heading2', element: 'h3', label: 'Heading 2' },
{ id: 'heading3', element: 'h4', label: 'Heading 3' }
{ modelElement: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
{ modelElement: 'heading1', viewElement: 'h2', title: 'Heading 1', class: 'ck-heading_heading1' },
{ modelElement: 'heading2', viewElement: 'h3', title: 'Heading 2', class: 'ck-heading_heading2' },
{ modelElement: 'heading3', viewElement: 'h4', title: 'Heading 3', class: 'ck-heading_heading3' }
]

@@ -57,25 +60,24 @@ } );

const editing = editor.editing;
const options = this._getLocalizedOptions();
const options = editor.config.get( 'heading.options' );
for ( let option of options ) {
// Skip paragraph - it is defined in required Paragraph feature.
if ( option.id !== defaultOptionId ) {
if ( option.modelElement !== defaultModelElement ) {
// Schema.
editor.document.schema.registerItem( option.id, '$block' );
editor.document.schema.registerItem( option.modelElement, '$block' );
// Build converter from model to view for data and editing pipelines.
buildModelConverter().for( data.modelToView, editing.modelToView )
.fromElement( option.id )
.toElement( option.element );
.fromElement( option.modelElement )
.toElement( option.viewElement );
// Build converter from view to model for data pipeline.
buildViewConverter().for( data.viewToModel )
.fromElement( option.element )
.toElement( option.id );
.fromElement( option.viewElement )
.toElement( option.modelElement );
// Register the heading command for this option.
editor.commands.set( option.modelElement, new HeadingCommand( editor, option ) );
}
}
// Register the heading command.
const command = new HeadingCommand( editor, options, defaultOptionId );
editor.commands.set( 'heading', command );
}

@@ -90,5 +92,4 @@

const editor = this.editor;
const command = editor.commands.get( 'heading' );
const enterCommand = editor.commands.get( 'enter' );
const options = this._getLocalizedOptions();
const options = editor.config.get( 'heading.options' );

@@ -99,6 +100,6 @@ if ( enterCommand ) {

const batch = data.batch;
const isHeading = options.some( option => option.id == positionParent.name );
const isHeading = options.some( option => positionParent.is( option.modelElement ) );
if ( isHeading && positionParent.name != command.defaultOption.id && positionParent.childCount === 0 ) {
batch.rename( positionParent, command.defaultOption.id );
if ( isHeading && !positionParent.is( defaultModelElement ) && positionParent.childCount === 0 ) {
batch.rename( positionParent, defaultModelElement );
}

@@ -108,50 +109,2 @@ } );

}
/**
* Returns heading options as defined in `config.heading.options` but processed to consider
* editor localization, i.e. to display {@link module:heading/headingcommand~HeadingOption#label}
* in the correct language.
*
* Note: The reason behind this method is that there's no way to use {@link utils/locale~Locale#t}
* when the user config is defined because the editor does not exist yet.
*
* @private
* @returns {Array.<module:heading/headingcommand~HeadingOption>}.
*/
_getLocalizedOptions() {
if ( this._cachedLocalizedOptions ) {
return this._cachedLocalizedOptions;
}
const editor = this.editor;
const t = editor.t;
const localizedLabels = {
Paragraph: t( 'Paragraph' ),
'Heading 1': t( 'Heading 1' ),
'Heading 2': t( 'Heading 2' ),
'Heading 3': t( 'Heading 3' )
};
/**
* Cached localized version of `config.heading.options` generated by
* {@link module:heading/headingengine~HeadingEngine#_localizedOptions}.
*
* @private
* @readonly
* @member {Array.<module:heading/headingcommand~HeadingOption>} #_cachedLocalizedOptions
*/
this._cachedLocalizedOptions = editor.config.get( 'heading.options' )
.map( option => {
if ( localizedLabels[ option.label ] ) {
// Clone the option to avoid altering the original `config.heading.options`.
option = Object.assign( {}, option, {
label: localizedLabels[ option.label ]
} );
}
return option;
} );
return this._cachedLocalizedOptions;
}
}

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

import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
add( 'pl', {
'Choose heading': 'Wybierz nagłówek',
'Paragraph': 'Akapit',
'Heading': 'Nagłówek',
'Heading 1': 'Nagłówek 1',

@@ -25,6 +28,6 @@ 'Heading 2': 'Nagłówek 2',

describe( 'Heading', () => {
let editor, dropdown;
let editor, editorElement, dropdown;
beforeEach( () => {
const editorElement = document.createElement( 'div' );
editorElement = document.createElement( 'div' );
document.body.appendChild( editorElement );

@@ -39,2 +42,5 @@

dropdown = editor.ui.componentFactory.create( 'headings' );
// Set data so the commands will be enabled.
setData( editor.document, '<paragraph>f{}oo</paragraph>' );
} );

@@ -44,2 +50,4 @@ } );

afterEach( () => {
editorElement.remove();
return editor.destroy();

@@ -64,2 +72,3 @@ } );

expect( dropdown.buttonView.label ).to.equal( 'Paragraph' );
expect( dropdown.buttonView.tooltip ).to.equal( 'Heading' );
} );

@@ -71,7 +80,7 @@

dropdown.id = 'foo';
dropdown.commandName = 'paragraph';
dropdown.fire( 'execute' );
sinon.assert.calledOnce( executeSpy );
sinon.assert.calledWithExactly( executeSpy, 'heading', { id: 'foo' } );
sinon.assert.calledWithExactly( executeSpy, 'paragraph' );
} );

@@ -83,2 +92,3 @@

dropdown.commandName = 'paragraph';
dropdown.fire( 'execute' );

@@ -89,19 +99,38 @@

it( 'should add custom CSS class to dropdown', () => {
const dropdown = editor.ui.componentFactory.create( 'headings' );
expect( dropdown.element.classList.contains( 'ck-heading-dropdown' ) ).to.be.true;
} );
describe( 'model to command binding', () => {
let command;
let commands;
beforeEach( () => {
command = editor.commands.get( 'heading' );
commands = {};
editor.config.get( 'heading.options' ).forEach( ( { modelElement } ) => {
commands[ modelElement ] = editor.commands.get( modelElement );
} );
} );
it( 'isEnabled', () => {
for ( let name in commands ) {
commands[ name ].isEnabled = false;
}
expect( dropdown.buttonView.isEnabled ).to.be.false;
commands.heading2.isEnabled = true;
expect( dropdown.buttonView.isEnabled ).to.be.true;
command.isEnabled = false;
expect( dropdown.buttonView.isEnabled ).to.be.false;
} );
it( 'label', () => {
expect( dropdown.buttonView.label ).to.equal( 'Paragraph' );
command.value = command.options[ 1 ];
expect( dropdown.buttonView.label ).to.equal( 'Heading 1' );
for ( let name in commands ) {
commands[ name ].value = false;
}
expect( dropdown.buttonView.label ).to.equal( 'Choose heading' );
commands.heading2.value = true;
expect( dropdown.buttonView.label ).to.equal( 'Heading 2' );
} );

@@ -111,24 +140,10 @@ } );

describe( 'localization', () => {
let command;
let commands, editor, dropdown;
beforeEach( () => {
const editorElement = document.createElement( 'div' );
return ClassicTestEditor.create( editorElement, {
plugins: [ Heading ],
toolbar: [ 'heading' ],
lang: 'pl',
heading: {
options: [
{ id: 'paragraph', element: 'p', label: 'Paragraph' },
{ id: 'heading1', element: 'h2', label: 'Heading 1' },
{ id: 'heading2', element: 'h3', label: 'Not automatically localized' }
]
}
} )
.then( newEditor => {
editor = newEditor;
dropdown = editor.ui.componentFactory.create( 'headings' );
command = editor.commands.get( 'heading' );
} );
return localizedEditor( [
{ modelElement: 'paragraph', title: 'Paragraph' },
{ modelElement: 'heading1', viewElement: 'h2', title: 'Heading 1' },
{ modelElement: 'heading2', viewElement: 'h3', title: 'Heading 2' }
] );
} );

@@ -138,5 +153,5 @@

expect( editor.config.get( 'heading.options' ) ).to.deep.equal( [
{ id: 'paragraph', element: 'p', label: 'Paragraph' },
{ id: 'heading1', element: 'h2', label: 'Heading 1' },
{ id: 'heading2', element: 'h3', label: 'Not automatically localized' }
{ modelElement: 'paragraph', title: 'Paragraph' },
{ modelElement: 'heading1', viewElement: 'h2', title: 'Heading 1' },
{ modelElement: 'heading2', viewElement: 'h3', title: 'Heading 2' }
] );

@@ -148,4 +163,10 @@ } );

expect( buttonView.label ).to.equal( 'Wybierz nagłówek' );
expect( buttonView.tooltip ).to.equal( 'Nagłówek' );
commands.paragraph.value = true;
expect( buttonView.label ).to.equal( 'Akapit' );
command.value = command.options[ 1 ];
commands.paragraph.value = false;
commands.heading1.value = true;
expect( buttonView.label ).to.equal( 'Nagłówek 1' );

@@ -160,7 +181,86 @@ } );

'Nagłówek 1',
'Not automatically localized'
'Nagłówek 2'
] );
} );
it( 'allows custom titles', () => {
return localizedEditor( [
{ modelElement: 'paragraph', title: 'Custom paragraph title' },
{ modelElement: 'heading1', title: 'Custom heading1 title' }
] ).then( () => {
const listView = dropdown.listView;
expect( listView.items.map( item => item.label ) ).to.deep.equal( [
'Custom paragraph title',
'Custom heading1 title',
] );
} );
} );
it( 'translates default using the the locale', () => {
return localizedEditor( [
{ modelElement: 'paragraph', title: 'Paragraph' }
] ).then( () => {
const listView = dropdown.listView;
expect( listView.items.map( item => item.label ) ).to.deep.equal( [
'Akapit'
] );
} );
} );
function localizedEditor( options ) {
const editorElement = document.createElement( 'div' );
document.body.appendChild( editorElement );
return ClassicTestEditor.create( editorElement, {
plugins: [ Heading ],
toolbar: [ 'heading' ],
lang: 'pl',
heading: {
options: options
}
} )
.then( newEditor => {
editor = newEditor;
dropdown = editor.ui.componentFactory.create( 'headings' );
commands = {};
editor.config.get( 'heading.options' ).forEach( ( { modelElement } ) => {
commands[ modelElement ] = editor.commands.get( modelElement );
} );
editorElement.remove();
return editor.destroy();
} );
}
} );
describe( 'class', () => {
it( 'is set for the listView#items in the panel', () => {
const listView = dropdown.listView;
expect( listView.items.map( item => item.class ) ).to.deep.equal( [
'ck-heading_paragraph',
'ck-heading_heading1',
'ck-heading_heading2',
'ck-heading_heading3'
] );
} );
it( 'reflects the #value of the commands', () => {
const listView = dropdown.listView;
setData( editor.document, '<heading2>f{}oo</heading2>' );
expect( listView.items.map( item => item.isActive ) ).to.deep.equal( [
false,
false,
true,
false
] );
} );
} );
} );
} );

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

import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
import ParagraphCommand from '@ckeditor/ckeditor5-paragraph/src/paragraphcommand';
import HeadingCommand from '../src/headingcommand';

@@ -13,10 +14,9 @@ import Range from '@ckeditor/ckeditor5-engine/src/model/range';

const options = [
{ id: 'paragraph', element: 'p' },
{ id: 'heading1', element: 'h2' },
{ id: 'heading2', element: 'h3' },
{ id: 'heading3', element: 'h4' }
{ modelElement: 'heading1', viewElement: 'h2', title: 'H2' },
{ modelElement: 'heading2', viewElement: 'h3', title: 'H3' },
{ modelElement: 'heading3', viewElement: 'h4', title: 'H4' }
];
describe( 'HeadingCommand', () => {
let editor, document, command, root, schema;
let editor, document, commands, root, schema;

@@ -27,9 +27,17 @@ beforeEach( () => {

document = editor.document;
command = new HeadingCommand( editor, options, 'paragraph' );
commands = {};
schema = document.schema;
editor.commands.set( 'paragraph', new ParagraphCommand( editor ) );
schema.registerItem( 'paragraph', '$block' );
for ( let option of options ) {
schema.registerItem( option.id, '$block' );
commands[ option.modelElement ] = new HeadingCommand( editor, option );
schema.registerItem( option.modelElement, '$block' );
}
schema.registerItem( 'notBlock' );
schema.allow( { name: 'notBlock', inside: '$root' } );
schema.allow( { name: '$text', inside: 'notBlock' } );
root = document.getRoot();

@@ -40,5 +48,21 @@ } );

afterEach( () => {
command.destroy();
for ( let modelElement in commands ) {
commands[ modelElement ].destroy();
}
} );
describe( 'basic properties', () => {
for ( let option of options ) {
test( option );
}
function test( { modelElement, viewElement, title } ) {
it( `are set for option.modelElement = ${ modelElement }`, () => {
expect( commands[ modelElement ].modelElement ).to.equal( modelElement );
expect( commands[ modelElement ].viewElement ).to.equal( viewElement );
expect( commands[ modelElement ].title ).to.equal( title );
} );
}
} );
describe( 'value', () => {

@@ -49,20 +73,41 @@ for ( let option of options ) {

function test( option ) {
it( `equals ${ option.id } when collapsed selection is placed inside ${ option.id } element`, () => {
setData( document, `<${ option.id }>foobar</${ option.id }>` );
function test( { modelElement } ) {
it( `equals ${ modelElement } when collapsed selection is placed inside ${ modelElement } element`, () => {
setData( document, `<${ modelElement }>foobar</${ modelElement }>` );
const element = root.getChild( 0 );
document.selection.addRange( Range.createFromParentsAndOffsets( element, 3, element, 3 ) );
expect( command.value ).to.equal( option );
expect( commands[ modelElement ].value ).to.be.true;
} );
}
it( 'should be equal to #defaultOption if option has not been found', () => {
schema.registerItem( 'div', '$block' );
setData( document, '<div>xyz</div>' );
const element = root.getChild( 0 );
document.selection.addRange( Range.createFromParentsAndOffsets( element, 1, element, 1 ) );
it( `equals false if inside to non-block element`, () => {
setData( document, `<notBlock>[foo]</notBlock>` );
expect( command.value ).to.equal( command.defaultOption );
} );
expect( commands[ modelElement ].value ).to.be.false;
} );
it( `equals false if moved from ${ modelElement } to non-block element`, () => {
setData( document, `<${ modelElement }>[foo]</${ modelElement }><notBlock>foo</notBlock>` );
const element = document.getRoot().getChild( 1 );
document.enqueueChanges( () => {
document.selection.setRanges( [ Range.createIn( element ) ] );
} );
expect( commands[ modelElement ].value ).to.be.false;
} );
it( 'should be refreshed after calling refreshValue()', () => {
const command = commands[ modelElement ];
setData( document, `<${ modelElement }>[foo]</${ modelElement }><notBlock>foo</notBlock>` );
const element = document.getRoot().getChild( 1 );
// Purposely not putting it in `document.enqueueChanges` to update command manually.
document.selection.setRanges( [ Range.createIn( element ) ] );
expect( command.value ).to.be.true;
command.refreshValue();
expect( command.value ).to.be.false;
} );
}
} );

@@ -72,9 +117,9 @@

it( 'should update value after execution', () => {
const command = commands.heading1;
setData( document, '<paragraph>[]</paragraph>' );
command._doExecute( { id: 'heading1' } );
command._doExecute();
expect( getData( document ) ).to.equal( '<heading1>[]</heading1>' );
expect( command.value ).to.be.object;
expect( command.value.id ).to.equal( 'heading1' );
expect( command.value.element ).to.equal( 'h2' );
expect( command.value ).to.be.true;
} );

@@ -85,2 +130,4 @@

const batch = editor.document.batch();
const command = commands.heading1;
setData( document, '<paragraph>foo[]bar</paragraph>' );

@@ -94,2 +141,15 @@

} );
it( 'should use provided batch (converting to default option)', () => {
const batch = editor.document.batch();
const command = commands.heading1;
setData( document, '<heading1>foo[]bar</heading1>' );
expect( batch.deltas.length ).to.equal( 0 );
command._doExecute( { batch } );
expect( batch.deltas.length ).to.be.above( 0 );
} );
} );

@@ -105,3 +165,5 @@

it( 'uses paragraph as default value', () => {
it( 'converts to default option when executed with already applied option', () => {
const command = commands.heading1;
setData( document, '<heading1>foo[]bar</heading1>' );

@@ -113,9 +175,2 @@ command._doExecute();

it( 'converts to default option when executed with already applied option', () => {
setData( document, '<heading1>foo[]bar</heading1>' );
command._doExecute( { id: 'heading1' } );
expect( getData( document ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
} );
it( 'converts topmost blocks', () => {

@@ -125,14 +180,14 @@ schema.registerItem( 'inlineImage', '$inline' );

setData( document, '<heading1><inlineImage>foo[]</inlineImage>bar</heading1>' );
command._doExecute( { id: 'heading1' } );
setData( document, '<paragraph><inlineImage>foo[]</inlineImage>bar</paragraph>' );
commands.heading1._doExecute();
expect( getData( document ) ).to.equal( '<paragraph><inlineImage>foo[]</inlineImage>bar</paragraph>' );
expect( getData( document ) ).to.equal( '<heading1><inlineImage>foo[]</inlineImage>bar</heading1>' );
} );
function test( from, to ) {
it( `converts ${ from.id } to ${ to.id } on collapsed selection`, () => {
setData( document, `<${ from.id }>foo[]bar</${ from.id }>` );
command._doExecute( { id: to.id } );
it( `converts ${ from.modelElement } to ${ to.modelElement } on collapsed selection`, () => {
setData( document, `<${ from.modelElement }>foo[]bar</${ from.modelElement }>` );
commands[ to.modelElement ]._doExecute();
expect( getData( document ) ).to.equal( `<${ to.id }>foo[]bar</${ to.id }>` );
expect( getData( document ) ).to.equal( `<${ to.modelElement }>foo[]bar</${ to.modelElement }>` );
} );

@@ -151,7 +206,7 @@ }

it( 'converts all elements where selection is applied', () => {
setData( document, '<heading1>foo[</heading1><heading2>bar</heading2><heading2>]baz</heading2>' );
command._doExecute( { id: 'paragraph' } );
setData( document, '<heading1>foo[</heading1><heading2>bar</heading2><heading3>]baz</heading3>' );
commands.heading3._doExecute();
expect( getData( document ) ).to.equal(
'<paragraph>foo[</paragraph><paragraph>bar</paragraph><paragraph>]baz</paragraph>'
'<heading3>foo[</heading3><heading3>bar</heading3><heading3>]baz</heading3>'
);

@@ -162,3 +217,3 @@ } );

setData( document, '<heading1>foo[</heading1><heading1>bar</heading1><heading2>baz</heading2>]' );
command._doExecute( { id: 'heading1' } );
commands.heading1._doExecute();

@@ -170,8 +225,14 @@ expect( getData( document ) ).to.equal(

function test( from, to ) {
it( `converts ${ from.id } to ${ to.id } on non-collapsed selection`, () => {
setData( document, `<${ from.id }>foo[bar</${ from.id }><${ from.id }>baz]qux</${ from.id }>` );
command._doExecute( { id: to.id } );
function test( { modelElement: fromElement }, { modelElement: toElement } ) {
it( `converts ${ fromElement } to ${ toElement } on non-collapsed selection`, () => {
setData(
document,
`<${ fromElement }>foo[bar</${ fromElement }><${ fromElement }>baz]qux</${ fromElement }>`
);
expect( getData( document ) ).to.equal( `<${ to.id }>foo[bar</${ to.id }><${ to.id }>baz]qux</${ to.id }>` );
commands[ toElement ]._doExecute();
expect( getData( document ) ).to.equal(
`<${ toElement }>foo[bar</${ toElement }><${ toElement }>baz]qux</${ toElement }>`
);
} );

@@ -181,2 +242,36 @@ }

} );
describe( 'isEnabled', () => {
for ( let option of options ) {
test( option.modelElement );
}
function test( modelElement ) {
let command;
beforeEach( () => {
command = commands[ modelElement ];
} );
describe( `${ modelElement } command`, () => {
it( 'should be enabled when inside another block', () => {
setData( document, '<paragraph>f{}oo</paragraph>' );
expect( command.isEnabled ).to.be.true;
} );
it( 'should be disabled if inside non-block', () => {
setData( document, '<notBlock>f{}oo</notBlock>' );
expect( command.isEnabled ).to.be.false;
} );
it( 'should be disabled if selection is placed on non-block', () => {
setData( document, '[<notBlock>foo</notBlock>]' );
expect( command.isEnabled ).to.be.false;
} );
} );
}
} );
} );

@@ -7,16 +7,9 @@ /**

import HeadingEngine from '../src/headingengine';
import HeadingCommand from '../src/headingcommand';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import ParagraphCommand from '@ckeditor/ckeditor5-paragraph/src/paragraphcommand';
import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
import HeadingCommand from '../src/headingcommand';
import Enter from '@ckeditor/ckeditor5-enter/src/enter';
import { add } from '@ckeditor/ckeditor5-utils/src/translation-service';
import { getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
add( 'pl', {
'Paragraph': 'Akapit',
'Heading 1': 'Nagłówek 1',
'Heading 2': 'Nagłówek 2',
'Heading 3': 'Nagłówek 3',
} );
describe( 'HeadingEngine', () => {

@@ -58,7 +51,7 @@ let editor, document;

it( 'should register option command', () => {
expect( editor.commands.has( 'heading' ) ).to.be.true;
const command = editor.commands.get( 'heading' );
expect( command ).to.be.instanceOf( HeadingCommand );
it( 'should register #commands', () => {
expect( editor.commands.get( 'paragraph' ) ).to.be.instanceOf( ParagraphCommand );
expect( editor.commands.get( 'heading1' ) ).to.be.instanceOf( HeadingCommand );
expect( editor.commands.get( 'heading2' ) ).to.be.instanceOf( HeadingCommand );
expect( editor.commands.get( 'heading3' ) ).to.be.instanceOf( HeadingCommand );
} );

@@ -106,2 +99,8 @@

it( 'should not blow up if there\'s no enter command in the editor', () => {
return VirtualTestEditor.create( {
plugins: [ HeadingEngine ]
} );
} );
describe( 'config', () => {

@@ -112,6 +111,6 @@ describe( 'options', () => {

expect( editor.config.get( 'heading.options' ) ).to.deep.equal( [
{ id: 'paragraph', element: 'p', label: 'Paragraph' },
{ id: 'heading1', element: 'h2', label: 'Heading 1' },
{ id: 'heading2', element: 'h3', label: 'Heading 2' },
{ id: 'heading3', element: 'h4', label: 'Heading 3' }
{ modelElement: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
{ modelElement: 'heading1', viewElement: 'h2', title: 'Heading 1', class: 'ck-heading_heading1' },
{ modelElement: 'heading2', viewElement: 'h3', title: 'Heading 2', class: 'ck-heading_heading2' },
{ modelElement: 'heading3', viewElement: 'h4', title: 'Heading 3', class: 'ck-heading_heading3' }
] );

@@ -123,4 +122,4 @@ } );

const options = [
{ id: 'paragraph', element: 'p', label: 'Paragraph' },
{ id: 'h4', element: 'h4', label: 'H4' }
{ modelElement: 'paragraph', title: 'Paragraph' },
{ modelElement: 'h4', viewElement: 'h4', title: 'H4' }
];

@@ -137,3 +136,4 @@

expect( editor.commands.get( 'heading' ).options ).to.deep.equal( options );
expect( editor.commands.get( 'h4' ) ).to.be.instanceOf( HeadingCommand );
expect( editor.commands.get( 'paragraph' ) ).to.be.instanceOf( ParagraphCommand );

@@ -140,0 +140,0 @@ expect( document.schema.hasItem( 'paragraph' ) ).to.be.true;

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

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

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc