Skip to content

Commit

Permalink
Merge pull request #7835 from ckeditor/i/7794
Browse files Browse the repository at this point in the history
Feature (image): Introduced the insert image via URL feature. Closes #7794.
  • Loading branch information
Reinmar authored Aug 20, 2020
2 parents cdfcb2d + 10d6159 commit bb00c23
Show file tree
Hide file tree
Showing 20 changed files with 1,821 additions and 141 deletions.
7 changes: 6 additions & 1 deletion packages/ckeditor5-image/lang/contexts.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,10 @@
"Resize image to %0": "The label used for the standalone resize options buttons in the image toolbar",
"Resize image to the original size": "The accessibility label of the standalone image resize reset option button in the image toolbar for the screen readers",
"Original": "Default label for the resize option that resets the size of the image.",
"Image resize list": "The accessibility label of the image resize dropdown list for the screen readers."
"Image resize list": "The accessibility label of the image resize dropdown list for the screen readers.",
"Insert": "The label of submit form button if image src URL input has no value",
"Update": "The label of submit form button if image src URL input has value",
"Cancel": "The label of cancel form button",
"Insert image via URL": "The input label for the Insert image via URL form",
"Paste the image source URL.": "The tip label below the Insert image via URL form"
}
1 change: 1 addition & 0 deletions packages/ckeditor5-image/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@ckeditor/ckeditor5-basic-styles": "^21.0.0",
"@ckeditor/ckeditor5-block-quote": "^21.0.0",
"@ckeditor/ckeditor5-cloud-services": "^21.0.0",
"@ckeditor/ckeditor5-ckfinder": "^21.0.0",
"@ckeditor/ckeditor5-editor-classic": "^21.0.0",
"@ckeditor/ckeditor5-enter": "^21.0.0",
"@ckeditor/ckeditor5-easy-image": "^21.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ export default class ImageUploadCommand extends Command {
* @inheritDoc
*/
refresh() {
this.isEnabled = isImageAllowed( this.editor.model );
const imageElement = this.editor.model.document.selection.getSelectedElement();
const isImage = imageElement && imageElement.name === 'image' || false;

this.isEnabled = isImageAllowed( this.editor.model ) || isImage;
}

/**
Expand Down
168 changes: 144 additions & 24 deletions packages/ckeditor5-image/src/imageupload/imageuploadui.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,56 +8,176 @@
*/

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import ImageUploadPanelView from './ui/imageuploadpanelview';

import FileDialogButtonView from '@ckeditor/ckeditor5-upload/src/ui/filedialogbuttonview';
import { createImageTypeRegExp, prepareIntegrations } from './utils';

import imageIcon from '@ckeditor/ckeditor5-core/theme/icons/image.svg';
import { createImageTypeRegExp } from './utils';

import { isImage } from '../image/utils';

/**
* The image upload button plugin.
*
* For a detailed overview, check the {@glink features/image-upload/image-upload Image upload feature} documentation.
*
* Adds the `'imageUpload'` button to the {@link module:ui/componentfactory~ComponentFactory UI component factory}.
* Adds the `'imageUpload'` dropdown to the {@link module:ui/componentfactory~ComponentFactory UI component factory}.
*
* @extends module:core/plugin~Plugin
*/
export default class ImageUploadUI extends Plugin {
/**
* @inheritDoc
*/
static get pluginName() {
return 'ImageUploadUI';
}

/**
* @inheritDoc
*/
init() {
const editor = this.editor;
const t = editor.t;
const isImageUploadPanelViewEnabled = !!editor.config.get( 'image.upload.panel.items' );

// Setup `imageUpload` button.
editor.ui.componentFactory.add( 'imageUpload', locale => {
const view = new FileDialogButtonView( locale );
const command = editor.commands.get( 'imageUpload' );
const imageTypes = editor.config.get( 'image.upload.types' );
const imageTypesRegExp = createImageTypeRegExp( imageTypes );
if ( isImageUploadPanelViewEnabled ) {
return this._createDropdownView( locale );
} else {
return this._createFileDialogButtonView( locale );
}
} );
}

view.set( {
acceptedType: imageTypes.map( type => `image/${ type }` ).join( ',' ),
allowMultipleFiles: true
} );
/**
* Sets up the dropdown view.
*
* @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView A dropdownView.
* @param {module:image/imageupload/ui/imageuploadpanelview~ImageUploadPanelView} imageUploadView An imageUploadView.
* @param {module:core/command~Command} command An imageUpload command
*
* @private
* @returns {module:ui/dropdown/dropdownview~DropdownView}
*/
_setUpDropdown( dropdownView, imageUploadView, command ) {
const editor = this.editor;
const t = editor.t;
const insertButtonView = imageUploadView.insertButtonView;

view.buttonView.set( {
label: t( 'Insert image' ),
icon: imageIcon,
tooltip: true
} );
dropdownView.bind( 'isEnabled' ).to( command );

view.buttonView.bind( 'isEnabled' ).to( command );
dropdownView.on( 'change:isOpen', () => {
const selectedElement = editor.model.document.selection.getSelectedElement();

view.on( 'done', ( evt, files ) => {
const imagesToUpload = Array.from( files ).filter( file => imageTypesRegExp.test( file.type ) );
if ( dropdownView.isOpen ) {
imageUploadView.focus();

if ( imagesToUpload.length ) {
editor.execute( 'imageUpload', { file: imagesToUpload } );
if ( isImage( selectedElement ) ) {
imageUploadView.imageURLInputValue = selectedElement.getAttribute( 'src' );
insertButtonView.label = t( 'Update' );
} else {
imageUploadView.imageURLInputValue = '';
insertButtonView.label = t( 'Insert' );
}
} );
}
} );

imageUploadView.delegate( 'submit', 'cancel' ).to( dropdownView );
this.delegate( 'cancel' ).to( dropdownView );

dropdownView.on( 'submit', () => {
closePanel();
onSubmit();
} );

dropdownView.on( 'cancel', () => {
closePanel();
} );

function onSubmit() {
const selectedElement = editor.model.document.selection.getSelectedElement();

if ( isImage( selectedElement ) ) {
editor.model.change( writer => {
writer.setAttribute( 'src', imageUploadView.imageURLInputValue, selectedElement );
writer.removeAttribute( 'srcset', selectedElement );
writer.removeAttribute( 'sizes', selectedElement );
} );
} else {
editor.execute( 'imageInsert', { source: imageUploadView.imageURLInputValue } );
}
}

return view;
function closePanel() {
editor.editing.view.focus();
dropdownView.isOpen = false;
}

return dropdownView;
}

/**
* Creates the dropdown view.
*
* @param {module:utils/locale~Locale} locale The localization services instance.
*
* @private
* @returns {module:ui/dropdown/dropdownview~DropdownView}
*/
_createDropdownView( locale ) {
const editor = this.editor;
const imageUploadView = new ImageUploadPanelView( locale, prepareIntegrations( editor ) );
const command = editor.commands.get( 'imageUpload' );

const dropdownView = imageUploadView.dropdownView;
const panelView = dropdownView.panelView;
const splitButtonView = dropdownView.buttonView;

splitButtonView.actionView = this._createFileDialogButtonView( locale );

panelView.children.add( imageUploadView );

return this._setUpDropdown( dropdownView, imageUploadView, command );
}

/**
* Creates and sets up file dialog button view.
*
* @param {module:utils/locale~Locale} locale The localization services instance.
*
* @private
* @returns {module:upload/ui/filedialogbuttonview~FileDialogButtonView}
*/
_createFileDialogButtonView( locale ) {
const editor = this.editor;
const t = locale.t;
const imageTypes = editor.config.get( 'image.upload.types' );
const fileDialogButtonView = new FileDialogButtonView( locale );
const imageTypesRegExp = createImageTypeRegExp( imageTypes );
const command = editor.commands.get( 'imageUpload' );

fileDialogButtonView.set( {
acceptedType: imageTypes.map( type => `image/${ type }` ).join( ',' ),
allowMultipleFiles: true
} );

fileDialogButtonView.buttonView.set( {
label: t( 'Insert image' ),
icon: imageIcon,
tooltip: true
} );

fileDialogButtonView.buttonView.bind( 'isEnabled' ).to( command );

fileDialogButtonView.on( 'done', ( evt, files ) => {
const imagesToUpload = Array.from( files ).filter( file => imageTypesRegExp.test( file.type ) );

if ( imagesToUpload.length ) {
editor.execute( 'imageUpload', { file: imagesToUpload } );
}
} );

return fileDialogButtonView;
}
}
103 changes: 103 additions & 0 deletions packages/ckeditor5-image/src/imageupload/ui/imageuploadformrowview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/**
* @module image/imageupload/ui/imageuploadformrowview
*/

import View from '@ckeditor/ckeditor5-ui/src/view';

import '../../../theme/imageuploadformrowview.css';

/**
* The class representing a single row in a complex form,
* used by {@link module:image/imageupload/ui/imageuploadpanelview~ImageUploadPanelView}.
*
* **Note**: For now this class is private. When more use cases arrive (beyond ckeditor5-table and ckeditor5-image),
* it will become a component in ckeditor5-ui.
*
* @private
* @extends module:ui/view~View
*/
export default class ImageUploadFormRowView extends View {
/**
* Creates an instance of the form row class.
*
* @param {module:utils/locale~Locale} locale The locale instance.
* @param {Object} options
* @param {Array.<module:ui/view~View>} [options.children]
* @param {String} [options.class]
* @param {module:ui/view~View} [options.labelView] When passed, the row gets the `group` and `aria-labelledby`
* DOM attributes and gets described by the label.
*/
constructor( locale, options = {} ) {
super( locale );

const bind = this.bindTemplate;

/**
* An additional CSS class added to the {@link #element}.
*
* @observable
* @member {String} #class
*/
this.set( 'class', options.class || null );

/**
* A collection of row items (buttons, dropdowns, etc.).
*
* @readonly
* @member {module:ui/viewcollection~ViewCollection}
*/
this.children = this.createCollection();

if ( options.children ) {
options.children.forEach( child => this.children.add( child ) );
}

/**
* The role property reflected by the `role` DOM attribute of the {@link #element}.
*
* **Note**: Used only when a `labelView` is passed to constructor `options`.
*
* @private
* @observable
* @member {String} #role
*/
this.set( '_role', null );

/**
* The ARIA property reflected by the `aria-labelledby` DOM attribute of the {@link #element}.
*
* **Note**: Used only when a `labelView` is passed to constructor `options`.
*
* @private
* @observable
* @member {String} #ariaLabelledBy
*/
this.set( '_ariaLabelledBy', null );

if ( options.labelView ) {
this.set( {
_role: 'group',
_ariaLabelledBy: options.labelView.id
} );
}

this.setTemplate( {
tag: 'div',
attributes: {
class: [
'ck',
'ck-form__row',
bind.to( 'class' )
],
role: bind.to( '_role' ),
'aria-labelledby': bind.to( '_ariaLabelledBy' )
},
children: this.children
} );
}
}
Loading

0 comments on commit bb00c23

Please sign in to comment.