Skip to content

Commit

Permalink
Added ckeditor5-media-embed/_src.
Browse files Browse the repository at this point in the history
  • Loading branch information
arkflpc committed Feb 15, 2023
1 parent ffb8910 commit 305e035
Show file tree
Hide file tree
Showing 11 changed files with 1,912 additions and 0 deletions.
182 changes: 182 additions & 0 deletions packages/ckeditor5-media-embed/_src/automediaembed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/**
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/**
* @module media-embed/automediaembed
*/

import { Plugin } from 'ckeditor5/src/core';
import { LiveRange, LivePosition } from 'ckeditor5/src/engine';
import { Clipboard } from 'ckeditor5/src/clipboard';
import { Delete } from 'ckeditor5/src/typing';
import { Undo } from 'ckeditor5/src/undo';
import { global } from 'ckeditor5/src/utils';

import MediaEmbedEditing from './mediaembedediting';
import { insertMedia } from './utils';

const URL_REGEXP = /^(?:http(s)?:\/\/)?[\w-]+\.[\w-.~:/?#[\]@!$&'()*+,;=%]+$/;

/**
* The auto-media embed plugin. It recognizes media links in the pasted content and embeds
* them shortly after they are injected into the document.
*
* @extends module:core/plugin~Plugin
*/
export default class AutoMediaEmbed extends Plugin {
/**
* @inheritDoc
*/
static get requires() {
return [ Clipboard, Delete, Undo ];
}

/**
* @inheritDoc
*/
static get pluginName() {
return 'AutoMediaEmbed';
}

/**
* @inheritDoc
*/
constructor( editor ) {
super( editor );

/**
* The paste–to–embed `setTimeout` ID. Stored as a property to allow
* cleaning of the timeout.
*
* @private
* @member {Number} #_timeoutId
*/
this._timeoutId = null;

/**
* The position where the `<media>` element will be inserted after the timeout,
* determined each time the new content is pasted into the document.
*
* @private
* @member {module:engine/model/liveposition~LivePosition} #_positionToInsert
*/
this._positionToInsert = null;
}

/**
* @inheritDoc
*/
init() {
const editor = this.editor;
const modelDocument = editor.model.document;

// We need to listen on `Clipboard#inputTransformation` because we need to save positions of selection.
// After pasting, the content between those positions will be checked for a URL that could be transformed
// into media.
this.listenTo( editor.plugins.get( 'ClipboardPipeline' ), 'inputTransformation', () => {
const firstRange = modelDocument.selection.getFirstRange();

const leftLivePosition = LivePosition.fromPosition( firstRange.start );
leftLivePosition.stickiness = 'toPrevious';

const rightLivePosition = LivePosition.fromPosition( firstRange.end );
rightLivePosition.stickiness = 'toNext';

modelDocument.once( 'change:data', () => {
this._embedMediaBetweenPositions( leftLivePosition, rightLivePosition );

leftLivePosition.detach();
rightLivePosition.detach();
}, { priority: 'high' } );
} );

editor.commands.get( 'undo' ).on( 'execute', () => {
if ( this._timeoutId ) {
global.window.clearTimeout( this._timeoutId );
this._positionToInsert.detach();

this._timeoutId = null;
this._positionToInsert = null;
}
}, { priority: 'high' } );
}

/**
* Analyzes the part of the document between provided positions in search for a URL representing media.
* When the URL is found, it is automatically converted into media.
*
* @protected
* @param {module:engine/model/liveposition~LivePosition} leftPosition Left position of the selection.
* @param {module:engine/model/liveposition~LivePosition} rightPosition Right position of the selection.
*/
_embedMediaBetweenPositions( leftPosition, rightPosition ) {
const editor = this.editor;
const mediaRegistry = editor.plugins.get( MediaEmbedEditing ).registry;
// TODO: Use marker instead of LiveRange & LivePositions.
const urlRange = new LiveRange( leftPosition, rightPosition );
const walker = urlRange.getWalker( { ignoreElementEnd: true } );

let url = '';

for ( const node of walker ) {
if ( node.item.is( '$textProxy' ) ) {
url += node.item.data;
}
}

url = url.trim();

// If the URL does not match to universal URL regexp, let's skip that.
if ( !url.match( URL_REGEXP ) ) {
urlRange.detach();

return;
}

// If the URL represents a media, let's use it.
if ( !mediaRegistry.hasMedia( url ) ) {
urlRange.detach();

return;
}

const mediaEmbedCommand = editor.commands.get( 'mediaEmbed' );

// Do not anything if media element cannot be inserted at the current position (#47).
if ( !mediaEmbedCommand.isEnabled ) {
urlRange.detach();

return;
}

// Position won't be available in the `setTimeout` function so let's clone it.
this._positionToInsert = LivePosition.fromPosition( leftPosition );

// This action mustn't be executed if undo was called between pasting and auto-embedding.
this._timeoutId = global.window.setTimeout( () => {
editor.model.change( writer => {
this._timeoutId = null;

writer.remove( urlRange );
urlRange.detach();

let insertionPosition;

// Check if position where the media element should be inserted is still valid.
// Otherwise leave it as undefined to use document.selection - default behavior of model.insertContent().
if ( this._positionToInsert.root.rootName !== '$graveyard' ) {
insertionPosition = this._positionToInsert;
}

insertMedia( editor.model, url, insertionPosition, false );

this._positionToInsert.detach();
this._positionToInsert = null;
} );

editor.plugins.get( 'Delete' ).requestUndoOnBackspace();
}, 100 );
}
}
60 changes: 60 additions & 0 deletions packages/ckeditor5-media-embed/_src/converters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/**
* @module media-embed/converters
*/

/**
* Returns a function that converts the model "url" attribute to the view representation.
*
* Depending on the configuration, the view representation can be "semantic" (for the data pipeline):
*
* <figure class="media">
* <oembed url="foo"></oembed>
* </figure>
*
* or "non-semantic" (for the editing view pipeline):
*
* <figure class="media">
* <div data-oembed-url="foo">[ non-semantic media preview for "foo" ]</div>
* </figure>
*
* **Note:** Changing the model "url" attribute replaces the entire content of the
* `<figure>` in the view.
*
* @param {module:media-embed/mediaregistry~MediaRegistry} registry The registry providing
* the media and their content.
* @param {Object} options
* @param {String} [options.elementName] When set, overrides the default element name for semantic media embeds.
* @param {String} [options.renderMediaPreview] When `true`, the converter will create the view in the non-semantic form.
* @param {String} [options.renderForEditingView] When `true`, the converter will create a view specific for the
* editing pipeline (e.g. including CSS classes, content placeholders).
* @returns {Function}
*/
export function modelToViewUrlAttributeConverter( registry, options ) {
return dispatcher => {
dispatcher.on( 'attribute:url:media', converter );
};

function converter( evt, data, conversionApi ) {
if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
return;
}

const url = data.attributeNewValue;
const viewWriter = conversionApi.writer;
const figure = conversionApi.mapper.toViewElement( data.item );
const mediaContentElement = [ ...figure.getChildren() ]
.find( child => child.getCustomProperty( 'media-content' ) );

// TODO: removing the wrapper and creating it from scratch is a hack. We can do better than that.
viewWriter.remove( mediaContentElement );

const mediaViewElement = registry.getMediaViewElement( viewWriter, url, options );

viewWriter.insert( viewWriter.createPositionAt( figure, 0 ), mediaViewElement );
}
}
14 changes: 14 additions & 0 deletions packages/ckeditor5-media-embed/_src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/**
* @module media-embed
*/

export { default as MediaEmbed } from './mediaembed';
export { default as MediaEmbedEditing } from './mediaembedediting';
export { default as MediaEmbedUI } from './mediaembedui';
export { default as AutoMediaEmbed } from './automediaembed';
export { default as MediaEmbedToolbar } from './mediaembedtoolbar';
Loading

0 comments on commit 305e035

Please sign in to comment.