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 37
Merged
Image captioning #48
Changes from all commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
2d143c4
Added initial ImageCaptioningEngine and manual test.
szymonkups 6c83c6b
Added conversion for elements nested inside image figure element.
szymonkups 00318cb
Image captioning engine and manual test.
szymonkups 6196dc2
Image captioning converters.
szymonkups ffd123e
Use EditableElement for nested editables.
szymonkups 5eace2c
Added tests to captioning converters.
szymonkups 1bdf50e
Image captioning: model to view converter inserts figcaption at the e…
szymonkups b2eb5ed
Added support for nested editables when clicking on widget.
szymonkups 1db21af
Adding focus class to captioning nested editable.
szymonkups 6392d6e
Merge branch 'master' into t/28
szymonkups 67c38ed
Adding view caption element when image widget is selected.
szymonkups 291125b
Removing empty image caption when image widget is no longer selected.
szymonkups 586f8e6
Update image captioning focus style.
szymonkups afddbcf
Refactoring image captioning code.
szymonkups 4abc44f
More refactoring in image captioning.
szymonkups e987db7
Tests form ImageCaptioningEngine.
szymonkups 7ec7dcf
Added more test to ImageCaptioningEngine.
szymonkups 996df20
Added tests for image captioning utils.
szymonkups 064f957
More tests to ImageCaptioningEngine.
szymonkups 13bd1cc
Updated widget tests for integration with nested editables.
szymonkups eda5d7a
Docs fixes in image captioning.
szymonkups d3c0f59
Added tests for ImageCaptioning plugin.
szymonkups 766f0cb
Updated image captioning manual test.
szymonkups da46641
Merge branch 'master' into t/28
szymonkups 760d434
Merge branch 'master' into t/28
Reinmar 09bdb3f
Merge branch 'master' into t/28
szymonkups 35cb5ed
Fixed image converters tests.
szymonkups c290e29
Renamed ImageCaptioning to ImageCaption.
szymonkups e4a3a66
Minor fixes in ImageCaption.
szymonkups 045d7c4
Methods renaming, small fixes in ImageCaption.
szymonkups 5aa3ffa
Added more features to image caption manual test.
szymonkups 3ac2144
Removed focus class from image when caption editable is focused.
szymonkups b304d33
Better checking if clicked inside nested edtiable in widget.
szymonkups 9d6e6c7
Tests: Added list feature to the manual test.
Reinmar 2603f00
Other: Added caption to limits set in schema.
szymonkups f67581a
Removed mistakenly committed it.only().
Reinmar 86dbdf2
Added a comment about TODO.
Reinmar 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
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,26 @@ | ||
/** | ||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
*/ | ||
|
||
/** | ||
* @module image/imagecaption/imagecaption | ||
*/ | ||
|
||
import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; | ||
import ImageCaptionEngine from './imagecaptionengine'; | ||
import '../../theme/imagecaption/theme.scss'; | ||
|
||
/** | ||
* The image caption plugin. | ||
* | ||
* @extends module:core/plugin~Plugin | ||
*/ | ||
export default class ImageCaption extends Plugin { | ||
/** | ||
* @inheritDoc | ||
*/ | ||
static get requires() { | ||
return [ ImageCaptionEngine ]; | ||
} | ||
} |
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,225 @@ | ||
/** | ||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
*/ | ||
|
||
/** | ||
* @module image/imagecaption/imagecaptionengine | ||
*/ | ||
|
||
import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; | ||
import ModelTreeWalker from '@ckeditor/ckeditor5-engine/src/model/treewalker'; | ||
import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element'; | ||
import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement'; | ||
import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element'; | ||
import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position'; | ||
import ViewRange from '@ckeditor/ckeditor5-engine/src/view/range'; | ||
import viewWriter from '@ckeditor/ckeditor5-engine/src/view/writer'; | ||
import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position'; | ||
import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter'; | ||
import ViewMatcher from '@ckeditor/ckeditor5-engine/src/view/matcher'; | ||
import { isImage, isImageWidget } from '../utils'; | ||
import { captionElementCreator, isCaption, getCaptionFromImage } from './utils'; | ||
|
||
/** | ||
* The image caption engine plugin. | ||
* | ||
* Registers proper converters. Takes care of adding caption element if image without it is inserted to model document. | ||
* | ||
* @extends module:core/plugin~Plugin | ||
*/ | ||
export default class ImageCaptionEngine extends Plugin { | ||
/** | ||
* @inheritDoc | ||
*/ | ||
init() { | ||
const editor = this.editor; | ||
const document = editor.document; | ||
const viewDocument = editor.editing.view; | ||
const schema = document.schema; | ||
const data = editor.data; | ||
const editing = editor.editing; | ||
|
||
/** | ||
* Last selected caption editable. | ||
* It is used for hiding editable when is empty and image widget is no longer selected. | ||
* | ||
* @member {module:image/imagecaption/imagecaptionengine~ImageCaptionEngine} #_lastSelectedEditable | ||
*/ | ||
|
||
// Schema configuration. | ||
schema.registerItem( 'caption' ); | ||
schema.allow( { name: '$inline', inside: 'caption' } ); | ||
schema.allow( { name: 'caption', inside: 'image' } ); | ||
schema.limits.add( 'caption' ); | ||
|
||
// Add caption element to each image inserted without it. | ||
document.on( 'change', insertMissingCaptionElement ); | ||
|
||
// View to model converter for data pipeline. | ||
const matcher = new ViewMatcher( ( element ) => { | ||
const parent = element.parent; | ||
|
||
// Convert only captions for images. | ||
if ( element.name == 'figcaption' && parent && parent.name == 'figure' && parent.hasClass( 'image' ) ) { | ||
return { name: true }; | ||
} | ||
|
||
return null; | ||
} ); | ||
|
||
buildViewConverter() | ||
.for( data.viewToModel ) | ||
.from( matcher ) | ||
.toElement( 'caption' ); | ||
|
||
// Model to view converter for data pipeline. | ||
data.modelToView.on( | ||
'insert:caption', | ||
captionModelToView( new ViewContainerElement( 'figcaption' ) ) | ||
); | ||
|
||
// Model to view converter for editing pipeline. | ||
editing.modelToView.on( | ||
'insert:caption', | ||
captionModelToView( captionElementCreator( viewDocument ) ) | ||
); | ||
|
||
// Adding / removing caption element when there is no text in the model. | ||
const selection = viewDocument.selection; | ||
|
||
// Update view before each rendering. | ||
this.listenTo( viewDocument, 'render', () => { | ||
// Check if there is an empty caption view element to remove. | ||
this._removeEmptyCaption(); | ||
|
||
// Check if image widget is selected and caption view element needs to be added. | ||
this._addCaption(); | ||
|
||
// If selection is currently inside caption editable - store it to hide when empty. | ||
const editableElement = selection.editableElement; | ||
|
||
if ( editableElement && isCaption( selection.editableElement ) ) { | ||
this._lastSelectedEditable = selection.editableElement; | ||
} | ||
}, { priority: 'high' } ); | ||
} | ||
|
||
/** | ||
* Checks if there is an empty caption element to remove from view. | ||
* | ||
* @private | ||
*/ | ||
_removeEmptyCaption() { | ||
const viewSelection = this.editor.editing.view.selection; | ||
const viewCaptionElement = this._lastSelectedEditable; | ||
|
||
// No caption to hide. | ||
if ( !viewCaptionElement ) { | ||
return; | ||
} | ||
|
||
// If selection is placed inside caption - do not remove it. | ||
if ( viewSelection.editableElement === viewCaptionElement ) { | ||
return; | ||
} | ||
|
||
// Do not remove caption if selection is placed on image that contains that caption. | ||
const selectedElement = viewSelection.getSelectedElement(); | ||
|
||
if ( selectedElement && isImageWidget( selectedElement ) ) { | ||
const viewImage = viewCaptionElement.findAncestor( element => element == selectedElement ); | ||
|
||
if ( viewImage ) { | ||
return; | ||
} | ||
} | ||
|
||
// Remove image caption if its empty. | ||
if ( viewCaptionElement.childCount === 0 ) { | ||
const mapper = this.editor.editing.mapper; | ||
viewWriter.remove( ViewRange.createOn( viewCaptionElement ) ); | ||
mapper.unbindViewElement( viewCaptionElement ); | ||
} | ||
} | ||
|
||
/** | ||
* Checks if selected image needs a new caption element inside. | ||
* | ||
* @private | ||
*/ | ||
_addCaption() { | ||
const editing = this.editor.editing; | ||
const selection = editing.view.selection; | ||
const imageFigure = selection.getSelectedElement(); | ||
const mapper = editing.mapper; | ||
const editableCreator = captionElementCreator( editing.view ); | ||
|
||
if ( imageFigure && isImageWidget( imageFigure ) ) { | ||
const modelImage = mapper.toModelElement( imageFigure ); | ||
const modelCaption = getCaptionFromImage( modelImage ); | ||
let viewCaption = mapper.toViewElement( modelCaption ); | ||
|
||
if ( !viewCaption ) { | ||
viewCaption = editableCreator(); | ||
|
||
const viewPosition = ViewPosition.createAt( imageFigure, 'end' ); | ||
mapper.bindElements( modelCaption, viewCaption ); | ||
viewWriter.insert( viewPosition, viewCaption ); | ||
} | ||
|
||
this._lastSelectedEditable = viewCaption; | ||
} | ||
} | ||
} | ||
|
||
// Checks whether data inserted to the model document have image element that has no caption element inside it. | ||
// If there is none - adds it to the image element. | ||
// | ||
// @private | ||
function insertMissingCaptionElement( evt, changeType, data, batch ) { | ||
if ( changeType !== 'insert' ) { | ||
return; | ||
} | ||
|
||
const walker = new ModelTreeWalker( { | ||
boundaries: data.range, | ||
ignoreElementEnd: true | ||
} ); | ||
|
||
for ( let value of walker ) { | ||
const item = value.item; | ||
|
||
if ( value.type == 'elementStart' && isImage( item ) && !getCaptionFromImage( item ) ) { | ||
batch.document.enqueueChanges( () => { | ||
batch.insert( ModelPosition.createAt( item, 'end' ), new ModelElement( 'caption' ) ); | ||
} ); | ||
} | ||
} | ||
} | ||
|
||
// Creates a converter that converts image caption model element to view element. | ||
// | ||
// @private | ||
// @param {Function|module:engine/view/element~Element} elementCreator | ||
// @return {Function} | ||
function captionModelToView( elementCreator ) { | ||
return ( evt, data, consumable, conversionApi ) => { | ||
const captionElement = data.item; | ||
|
||
if ( isImage( captionElement.parent ) && ( captionElement.childCount > 0 ) ) { | ||
if ( !consumable.consume( data.item, 'insert' ) ) { | ||
return; | ||
} | ||
|
||
const imageFigure = conversionApi.mapper.toViewElement( data.range.start.parent ); | ||
const viewElement = ( elementCreator instanceof ViewElement ) ? | ||
elementCreator.clone( true ) : | ||
elementCreator( data, consumable, conversionApi ); | ||
|
||
const viewPosition = ViewPosition.createAt( imageFigure, 'end' ); | ||
conversionApi.mapper.bindElements( data.item, viewElement ); | ||
viewWriter.insert( viewPosition, viewElement ); | ||
} | ||
}; | ||
} |
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,63 @@ | ||
/** | ||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
*/ | ||
|
||
/** | ||
* @module image/imagecaption/utils | ||
*/ | ||
|
||
import ViewEditableElement from '@ckeditor/ckeditor5-engine/src/view/editableelement'; | ||
import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element'; | ||
|
||
const captionSymbol = Symbol( 'imageCaption' ); | ||
|
||
/** | ||
* Returns a function that creates caption editable element for the given {@link module:engine/view/document~Document}. | ||
* | ||
* @param {module:engine/view/document~Document} viewDocument | ||
* @return {Function} | ||
*/ | ||
export function captionElementCreator( viewDocument ) { | ||
return () => { | ||
const editable = new ViewEditableElement( 'figcaption', { contenteditable: true } ); | ||
editable.document = viewDocument; | ||
editable.setCustomProperty( captionSymbol, true ); | ||
|
||
editable.on( 'change:isFocused', ( evt, property, is ) => { | ||
if ( is ) { | ||
editable.addClass( 'focused' ); | ||
} else { | ||
editable.removeClass( 'focused' ); | ||
} | ||
} ); | ||
|
||
return editable; | ||
}; | ||
} | ||
|
||
/** | ||
* Returns `true` if given view element is image's caption editable. | ||
* | ||
* @param {module:engine/view/element~Element} viewElement | ||
* @return {Boolean} | ||
*/ | ||
export function isCaption( viewElement ) { | ||
return !!viewElement.getCustomProperty( captionSymbol ); | ||
} | ||
|
||
/** | ||
* Returns caption's model element from given image element. Returns `null` if no caption is found. | ||
* | ||
* @param {module:engine/model/element~Element} imageModelElement | ||
* @return {module:engine/model/element~Element|null} | ||
*/ | ||
export function getCaptionFromImage( imageModelElement ) { | ||
for ( let node of imageModelElement.getChildren() ) { | ||
if ( node instanceof ModelElement && node.name == 'caption' ) { | ||
return node; | ||
} | ||
} | ||
|
||
return null; | ||
} |
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.
Please link to https://github.com/ckeditor/ckeditor5-engine/issues/736