Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge pull request #5 from ckeditor/t/1
Browse files Browse the repository at this point in the history
Feature: Introduced the block quote feature. Closes #1.
  • Loading branch information
scofalik authored Mar 30, 2017
2 parents c9fbbfc + 3d94a93 commit 239015b
Show file tree
Hide file tree
Showing 17 changed files with 1,324 additions and 8 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Changelog
=========

## 0.0.1 (2017-03-06)

Internal changes only (updated dependencies, documentation, etc.).
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Software License Agreement
==========================

**CKEditor 5 Message Quote Feature**https://github.com/ckeditor/ckeditor5-paragraph <br>
**CKEditor 5 Block Quote Feature**https://github.com/ckeditor/ckeditor5-paragraph <br>
Copyright (c) 2003-2017, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.

Licensed under the terms of any of the following licenses at your choice:
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
CKEditor 5 Message Quote Feature
CKEditor 5 Block Quote Feature
========================================

[![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-quote.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-quote)
Expand All @@ -7,7 +7,7 @@ CKEditor 5 Message Quote Feature
[![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-quote/status.svg)](https://david-dm.org/ckeditor/ckeditor5-quote)
[![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-quote/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-quote?type=dev)

The message quote feature for CKEditor 5 project. More information about the project can be found at the following url: <https://github.com/ckeditor/ckeditor5>.
The block quote feature for CKEditor 5 project. More information about the project can be found at the following url: <https://github.com/ckeditor/ckeditor5>.

## License

Expand Down
3 changes: 3 additions & 0 deletions lang/contexts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"Block quote": "Toolbar button tooltip for the Block quote feature."
}
19 changes: 14 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
{
"name": "@ckeditor/ckeditor5-quote",
"name": "@ckeditor/ckeditor5-block-quote",
"version": "0.0.0",
"description": "Message quote feature for CKEditor 5.",
"description": "Block quote feature for CKEditor 5.",
"keywords": [
"CKEditor"
],
"dependencies": {
"@ckeditor/ckeditor5-core": "*",
"@ckeditor/ckeditor5-engine": "*",
"@ckeditor/ckeditor5-ui": "*",
"@ckeditor/ckeditor5-utils": "*"
},
"devDependencies": {
"@ckeditor/ckeditor5-dev-lint": "^2.0.0",
"@ckeditor/ckeditor5-dev-lint": "^2.0.2",
"@ckeditor/ckeditor5-editor-classic": "*",
"@ckeditor/ckeditor5-enter": "*",
"@ckeditor/ckeditor5-list": "*",
"@ckeditor/ckeditor5-paragraph": "*",
"@ckeditor/ckeditor5-presets": "*",
"gulp": "^3.9.0",
"guppy-pre-commit": "^0.4.0"
},
Expand All @@ -19,9 +28,9 @@
"author": "CKSource (http://cksource.com/)",
"license": "(GPL-2.0 OR LGPL-2.1 OR MPL-1.1)",
"homepage": "https://ckeditor5.github.io",
"bugs": "https://github.com/ckeditor/ckeditor5-quote/issues",
"bugs": "https://github.com/ckeditor/ckeditor5-block-quote/issues",
"repository": {
"type": "git",
"url": "https://github.com/ckeditor/ckeditor5-quote.git"
"url": "https://github.com/ckeditor/ckeditor5-block-quote.git"
}
}
86 changes: 86 additions & 0 deletions src/blockquote.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/**
* @module block-quote/blockquote
*/

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import BlockQuoteEngine from './blockquoteengine';

import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';

import quoteIcon from '@ckeditor/ckeditor5-core/theme/icons/quote.svg';
import '../theme/theme.scss';

/**
* The block quote plugin.
*
* It introduces the `'blockQuote'` button and requires {@link module:block-quote/blockquoteengine~BlockQuoteEngine}
* plugin. It also changes <kbd>Enter</kbd> key behavior so it escapes block quotes when pressed in an
* empty quoted block.
*
* @extends module:core/plugin~Plugin
*/
export default class BlockQuote extends Plugin {
/**
* @inheritDoc
*/
static get requires() {
return [ BlockQuoteEngine ];
}

/**
* @inheritDoc
*/
init() {
const editor = this.editor;
const t = editor.t;
const command = editor.commands.get( 'blockQuote' );

editor.ui.componentFactory.add( 'blockQuote', ( locale ) => {
const buttonView = new ButtonView( locale );

buttonView.set( {
label: t( 'Block quote' ),
icon: quoteIcon,
tooltip: true
} );

// Bind button model to command.
buttonView.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );

// Execute command.
this.listenTo( buttonView, 'execute', () => editor.execute( 'blockQuote' ) );

return buttonView;
} );
}

/**
* @inheritDoc
*/
afterInit() {
const editor = this.editor;
const command = editor.commands.get( 'blockQuote' );

// Overwrite default enter key behavior.
// If enter key is pressed with selection collapsed in empty block inside a quote, break the quote.
// This listener is added in afterInit in order to register it after list's feature listener.
// We can't use a priority for this, because 'low' is already used by the enter feature, unless
// we'd use numeric priority in this case.
this.listenTo( this.editor.editing.view, 'enter', ( evt, data ) => {
const doc = this.editor.document;
const positionParent = doc.selection.getLastPosition().parent;

if ( doc.selection.isCollapsed && positionParent.isEmpty && command.value ) {
this.editor.execute( 'blockQuote' );

data.preventDefault();
evt.stop();
}
} );
}
}
224 changes: 224 additions & 0 deletions src/blockquotecommand.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/**
* @module block-quote/blockquotecommand
*/

import Command from '@ckeditor/ckeditor5-core/src/command/command';
import Position from '@ckeditor/ckeditor5-engine/src/model/position';
import Element from '@ckeditor/ckeditor5-engine/src/model/element';
import Range from '@ckeditor/ckeditor5-engine/src/model/range';
import first from '@ckeditor/ckeditor5-utils/src/first';

/**
* The block quote command.
*
* @extends module:core/command/command~Command
*/
export default class BlockQuoteCommand extends Command {
/**
* @inheritDoc
*/
constructor( editor ) {
super( editor );

/**
* Flag indicating whether the command is active. It's on when the selection starts
* in a quoted block.
*
* @readonly
* @observable
* @member {Boolean} #value
*/
this.set( 'value', false );

// Update current value each time changes are done to the document.
this.listenTo( editor.document, 'changesDone', () => {
this.refreshValue();
this.refreshState();
} );
}

/**
* Updates command's {@link #value} based on the current selection.
*/
refreshValue() {
const firstBlock = first( this.editor.document.selection.getSelectedBlocks() );

// In the current implementation, the block quote must be an immediate parent of a block element.
this.value = !!( firstBlock && findQuote( firstBlock ) );
}

/**
* Executes the command. When the command {@link #value is on}, then all block quotes within
* the selection will be removed. If it's off, then all selected blocks will be wrapped with
* a block quote.
*
* @protected
* @param {Object} [options] Options for executed command.
* @param {module:engine/model/batch~Batch} [options.batch] Batch to collect all the change steps.
* New batch will be created if this option is not set.
*/
_doExecute( options = {} ) {
const doc = this.editor.document;
const batch = options.batch || doc.batch();
const blocks = Array.from( doc.selection.getSelectedBlocks() );

doc.enqueueChanges( () => {
if ( this.value ) {
this._removeQuote( batch, blocks.filter( findQuote ) );
} else {
this._applyQuote( batch, blocks );
}
} );
}

/**
* @inheritDoc
*/
_checkEnabled() {
if ( this.value ) {
return true;
}

const selection = this.editor.document.selection;
const schema = this.editor.document.schema;

const firstBlock = first( selection.getSelectedBlocks() );

if ( !firstBlock ) {
return false;
}

const isMQAllowed = schema.check( {
name: 'blockQuote',
inside: Position.createBefore( firstBlock )
} );
const isBlockAllowed = schema.check( {
name: firstBlock.name,
attributes: Array.from( firstBlock.getAttributeKeys() ),
inside: 'blockQuote'
} );

// Whether <mQ> can wrap the block.
return isMQAllowed && isBlockAllowed;
}

/**
* Removes the quote from the given blocks.
*
* If blocks which are supposed to be "unquoted" are in the middle of a quote,
* start it or end it, then the quote will be split (if needed) and the blocks
* will be moved out of it, so other quoted blocks remained quoted.
*
* @param {module:engine/model/batch~Batch} batch
* @param {Array.<module:engine/model/element~Element>} blocks
*/
_removeQuote( batch, blocks ) {
// Unquote all groups of block. Iterate in the reverse order to not break following ranges.
getRangesOfBlockGroups( blocks ).reverse().forEach( ( groupRange ) => {
if ( groupRange.start.isAtStart && groupRange.end.isAtEnd ) {
batch.unwrap( groupRange.start.parent );

return;
}

// The group of blocks are at the beginning of an <mQ> so let's move them left (out of the <mQ>).
if ( groupRange.start.isAtStart ) {
const positionBefore = Position.createBefore( groupRange.start.parent );

batch.move( groupRange, positionBefore );

return;
}

// The blocks are in the middle of an <mQ> so we need to split the <mQ> after the last block
// so we move the items there.
if ( !groupRange.end.isAtEnd ) {
batch.split( groupRange.end );
}

// Now we are sure that groupRange.end.isAtEnd is true, so let's move the blocks right.

const positionAfter = Position.createAfter( groupRange.end.parent );

batch.move( groupRange, positionAfter );
} );
}

/**
* Applies the quote to the given blocks.
*
* @param {module:engine/model/batch~Batch} batch
* @param {Array.<module:engine/model/element~Element>} blocks
*/
_applyQuote( batch, blocks ) {
const quotesToMerge = [];

// Quote all groups of block. Iterate in the reverse order to not break following ranges.
getRangesOfBlockGroups( blocks ).reverse().forEach( ( groupRange ) => {
let quote = findQuote( groupRange.start );

if ( !quote ) {
quote = new Element( 'blockQuote' );

batch.wrap( groupRange, quote );
}

quotesToMerge.push( quote );
} );

// Merge subsequent <mQ> elements. Reverse the order again because this time we want to go through
// the <mQ> elements in the source order (due to how merge works – it moves the right element's content
// to the first element and removes the right one. Since we may need to merge a couple of subsequent `<mQ>` elements
// we want to keep the reference to the first (furthest left) one.
quotesToMerge.reverse().reduce( ( currentQuote, nextQuote ) => {
if ( currentQuote.nextSibling == nextQuote ) {
batch.merge( Position.createAfter( currentQuote ) );

return currentQuote;
}

return nextQuote;
} );
}
}

function findQuote( elementOrPosition ) {
return elementOrPosition.parent.name == 'blockQuote' ? elementOrPosition.parent : null;
}

// Returns a minimal array of ranges containing groups of subsequent blocks.
//
// content: abcdefgh
// blocks: [ a, b, d , f, g, h ]
// output ranges: [ab]c[d]e[fgh]
//
// @param {Array.<module:engine/model/element~Element>} blocks
// @returns {Array.<module:engine/model/range~Range>}
function getRangesOfBlockGroups( blocks ) {
let startPosition;
let i = 0;
const ranges = [];

while ( i < blocks.length ) {
const block = blocks[ i ];
const nextBlock = blocks[ i + 1 ];

if ( !startPosition ) {
startPosition = Position.createBefore( block );
}

if ( !nextBlock || block.nextSibling != nextBlock ) {
ranges.push( new Range( startPosition, Position.createAfter( block ) ) );
startPosition = null;
}

i++;
}

return ranges;
}
Loading

0 comments on commit 239015b

Please sign in to comment.