This repository has been archived by the owner on Jun 26, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 5
Initial implementation of the block quote feature #5
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"Block quote": "Toolbar button tooltip for the Block quote feature." | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} ); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should not need to do tricks like this. There should be a way to to have both listeners working without having to think which should be registered when. We should take a look at lists listener if it can be fixed. If anything, we should consider adding list listener with higher priority.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't recall now, but AFAIR I checked this and the problem there was bigger ;< Basically, we had a too low granularity of priorities – there's enter feature listener, list feature and this – enter uses low, list uses normal. I don't want enter to use lowest because then you won't be able to postfix it (unless you use the same method as I did here). I'd leave this for now and wait for more such issues. Also, there's https://github.com/ckeditor/ckeditor5-link/issues/87 which is strongly related but for which I don't have energy now.
But I definitely have to report it in ckeditor5. I'll do that later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated my previous comment with more details.