From 2fb790d948e3cb894c8e0f9b08b1e31663ea7313 Mon Sep 17 00:00:00 2001 From: Tade0 Date: Fri, 29 May 2015 17:15:24 +0200 Subject: [PATCH 1/8] Added autolink and autoembed plugin. --- plugins/autoembed/plugin.js | 95 ++++++ plugins/autolink/plugin.js | 36 +++ tests/plugins/autoembed/autoembed.js | 270 ++++++++++++++++++ tests/plugins/autoembed/manual/autoembed.html | 33 +++ tests/plugins/autoembed/manual/autoembed.md | 13 + tests/plugins/autolink/autolink.js | 157 ++++++++++ 6 files changed, 604 insertions(+) create mode 100644 plugins/autoembed/plugin.js create mode 100644 plugins/autolink/plugin.js create mode 100644 tests/plugins/autoembed/autoembed.js create mode 100644 tests/plugins/autoembed/manual/autoembed.html create mode 100644 tests/plugins/autoembed/manual/autoembed.md create mode 100644 tests/plugins/autolink/autolink.js diff --git a/plugins/autoembed/plugin.js b/plugins/autoembed/plugin.js new file mode 100644 index 00000000000..8342c81f011 --- /dev/null +++ b/plugins/autoembed/plugin.js @@ -0,0 +1,95 @@ +/** + * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or http://ckeditor.com/license + */ + +'use strict'; + +( function() { + CKEDITOR.plugins.add( 'autoembed', { + requires: 'autolink,embed,undo', + + init: function( editor ) { + var currentId = 1; + + editor.on( 'paste', function( evt ) { + if ( evt.data.dataTransfer.getTransferType( editor ) == CKEDITOR.DATA_TRANSFER_INTERNAL ) { + return; + } + + var data = evt.data.dataValue, + parsedData, + link; + + // Expecting exactly one tag spanning the whole pasted content. + if ( data.match( /^$/i ) ) { + parsedData = CKEDITOR.htmlParser.fragment.fromHtml( data ); + + // Embed only links with a single text node with a href attr which equals its text. + if ( parsedData.children.length != 1 ) + return; + + link = parsedData.children[ 0 ]; + + if ( link.type == CKEDITOR.NODE_ELEMENT && link.getHtml() == link.attributes.href ) { + evt.data.dataValue = ' -1 ) { + return; + } + + // Regex by Imme Emosol. + data = data.replace( /^(https?|ftp):\/\/(-\.)?([^\s\/?\.#-]+\.?)+(\/[^\s]*)?[^\s\.,]$/ig , '$&' ); + + // If link was discovered, change the type to 'html'. This is important e.g. when pasting plain text in Chrome + // where real type is correctly recognized. + if ( data != evt.data.dataValue ) { + evt.data.type = 'html'; + } + + evt.data.dataValue = data; + } ); + } +} ); \ No newline at end of file diff --git a/tests/plugins/autoembed/autoembed.js b/tests/plugins/autoembed/autoembed.js new file mode 100644 index 00000000000..0291a4c41e2 --- /dev/null +++ b/tests/plugins/autoembed/autoembed.js @@ -0,0 +1,270 @@ +/* bender-tags: editor,unit */ +/* bender-ckeditor-plugins: embedbase,autoembed,enterkey,undo */ +/* bender-include: ../embedbase/_helpers/tools.js, ../clipboard/_helpers/pasting.js */ + +/* global embedTools, assertPasteEvent */ + +'use strict'; + +function correctJsonpCallback( urlTemplate, urlParams, callback ) { + callback( { + 'url': decodeURIComponent( urlParams.url ), + 'type': 'rich', + 'version': '1.0', + 'html': '' + } ); +} + +var jsonpCallback; + +embedTools.mockJsonp( function() { + jsonpCallback.apply( this, arguments ); +} ); + +bender.editor = { + creator: 'inline', + config: { + allowedContent: true + } +}; + +bender.test( { + setUp: function() { + jsonpCallback = correctJsonpCallback; + }, + + 'test working example': function() { + var bot = this.editorBot; + + this.editor.once( 'paste', function( evt ) { + assert.isMatching( /^https:\/\/foo.bar\/g\/200\/300<\/a>$/, evt.data.dataValue ); + }, null, null, 900 ); + + bot.setData( '

This is an embed

', function() { + bot.editor.focus(); + + var range = this.editor.createRange(); + range.setStart( this.editor.editable().findOne( 'p' ).getFirst(), 10 ); + range.collapse( true ); + this.editor.getSelection().selectRanges( [ range ] ); + + this.editor.execCommand( 'paste', 'https://foo.bar/g/200/300' ); + + // Note: afterPaste is fired asynchronously, but we can test editor data immediately. + assert.areSame( '

This is anhttps://foo.bar/g/200/300 embed

', bot.getData() ); + + wait( function() { + assert.areSame( '

This is an

embed

', bot.getData() ); + }, 200 ); + } ); + }, + + 'test embedding when request failed': function() { + var bot = this.editorBot; + jsonpCallback = function( urlTemplate, urlParams, callback, errorCallback ) { + errorCallback(); + }; + + bot.setData( '', function() { + bot.editor.focus(); + this.editor.execCommand( 'paste', 'https://foo.bar/g/200/302' ); + + // Note: afterPaste is fired asynchronously, but we can test editor data immediately. + assert.areSame( + '

https://foo.bar/g/200/302

', + bot.getData( 1 ), + 'link was pasted correctly' + ); + + wait( function() { + assert.areSame( + '

https://foo.bar/g/200/302

', + bot.getData( 1 ), + 'link was not auto embedded' + ); + }, 200 ); + } ); + }, + + 'test when user splits the link before the request is finished': function() { + var bot = this.editorBot; + + bot.setData( '', function() { + bot.editor.focus(); + this.editor.execCommand( 'paste', 'https://foo.bar/g/200/304' ); + + // Note: afterPaste is fired asynchronously, but we can test editor data immediately. + assert.areSame( '

https://foo.bar/g/200/304

', bot.getData( 1 ) ); + + var range = this.editor.createRange(); + range.setStart( this.editor.editable().findOne( 'a' ).getFirst(), 5 ); + range.setEnd( this.editor.editable().findOne( 'a' ).getFirst(), 8 ); + this.editor.getSelection().selectRanges( [ range ] ); + this.editor.execCommand( 'enter' ); + + assert.areSame( + '

https

foo.bar/g/200/304

', + bot.getData(), + 'enter key worked' + ); + + // It is not clear what should happen when the link was split, so we decided to embed only the first part. + wait( function() { + assert.areSame( + '
' + + '

foo.bar/g/200/304

', + bot.getData( 1 ), + 'the first part of the link was auto embedded' + ); + }, 200 ); + } ); + }, + + 'test uppercase link is auto embedded': function() { + var pastedText = 'https://foo.bar/bom', + expected = /^https:\/\/foo.bar\/bom<\/a>$/; + + assertPasteEvent( this.editor, { dataValue: pastedText }, function( data ) { + // Use prepInnerHtml to make sure attr are sorted. + assert.isMatching( expected, bender.tools.html.prepareInnerHtmlForComparison( data.dataValue ) ); + } ); + }, + + 'test link with attributes is auto embedded': function() { + var pastedText = 'https://foo.bar/bom', + expected = /^https:\/\/foo.bar\/bom<\/a>$/; + + assertPasteEvent( this.editor, { dataValue: pastedText }, function( data ) { + // Use prepInnerHtml to make sure attr are sorted. + assert.isMatching( expected, bender.tools.html.prepareInnerHtmlForComparison( data.dataValue ) ); + } ); + }, + + 'test anchor is not auto embedded': function() { + var pastedText = 'Not a link really.'; + + assertPasteEvent( this.editor, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } ); + }, + + // Because it means that user copied a linked text, not a link. + 'test link with text different than its href is not auto embedded': function() { + var pastedText = 'Foo bar.'; + + assertPasteEvent( this.editor, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } ); + }, + + // 'test undo': function() { + // var bot = this.editorBot, + // pastedText = 'https://foo.bar/g/200/382'; + + // bot.setData( '', function() { + // this.editor.execCommand( 'paste', pastedText ); + // assert.areSame( '

' + pastedText + '

', bot.getData() ); + + // this.editor.execCommand( 'undo' ); + + // wait( function() { + // assert.areSame( '', bot.getData() ); + // }, 200 ); + // } ); + // }, + + // 'test undo after embed': function() { + // var bot = this.editorBot, + // pastedText = 'https://foo.bar/g/200/382', + // that = this; + + // bot.setData( '', function() { + // this.editor.execCommand( 'paste', pastedText ); + // assert.areSame( '

' + pastedText + '

', bot.getData() ); + + // wait( function() { + // this.editor.execCommand( 'undo' ); + + // // The "cke-auto-embed" attribute should not be present. + // assert.isInnerHtmlMatching( '

' + + // pastedText + '[]

', bender.tools.selection.getWithHtml( that.editor ) ); + // }, 200 ); + // } ); + // }, + + // 'test double undo': function() { + // var bot = this.editorBot, + // pastedText = 'https://foo.bar/g/210/382'; + + // bot.setData( '

', function() { + // this.editor.execCommand( 'paste', pastedText ); + // assert.areSame( '

' + pastedText + '

', bot.getData() ); + + // wait( function() { + // assert.areSame( '
', bot.getData() ); + + // this.editor.execCommand( 'undo' ); + + // assert.areSame( '

' + pastedText + '

', bot.getData() ); + + // this.editor.execCommand( 'undo' ); + + // assert.areSame( '', bot.getData() ); + // }, 200 ); + // } ); + // }, + + // 'test undo and redo': function() { + // var bot = this.editorBot, + // pastedText = 'https://foo.bar/g/400/382', + // that = this; + + // bot.setData( '

', function() { + // this.editor.execCommand( 'paste', pastedText ); + // assert.areSame( '

' + pastedText + '

', bot.getData() ); + + // wait( function() { + // this.editor.execCommand( 'undo' ); + // assert.isInnerHtmlMatching( '

[]

', bender.tools.selection.getWithHtml( that.editor ) ); + + // this.editor.execCommand( 'redo' ); + // assert.isInnerHtmlMatching( [ '

' + + // pastedText + '[]

' ], bender.tools.selection.getWithHtml( that.editor ) ); + + // // Embedding never really happened, so an additional redo step should do nothing. + // this.editor.execCommand( 'redo' ); + // assert.isInnerHtmlMatching( [ '

' + + // pastedText + '[]

' ], bender.tools.selection.getWithHtml( that.editor ) ); + + // }, 50 ); // User fired undo before the link was embedded. + // } ); + // }, + + 'test internal paste is not auto embedded - text URL': function() { + var editor = this.editor, + pastedText = 'https://foo.bar/g/185/310'; + + this.editor.once( 'paste', function( evt ) { + evt.data.dataTransfer.sourceEditor = editor; + }, null, null, 1 ); + + this.editor.once( 'paste', function( evt ) { + evt.cancel(); + assert.areSame( pastedText, evt.data.dataValue ); + }, null, null, 900 ); + + this.editor.execCommand( 'paste', pastedText ); + }, + + 'test internal paste is not auto embedded - link': function() { + var editor = this.editor, + pastedText = 'https://foo.bar/g/185/310'; + + this.editor.once( 'paste', function( evt ) { + evt.data.dataTransfer.sourceEditor = editor; + }, null, null, 1 ); + + this.editor.once( 'paste', function( evt ) { + evt.cancel(); + assert.areSame( pastedText, evt.data.dataValue ); + }, null, null, 900 ); + + this.editor.execCommand( 'paste', pastedText ); + } +} ); diff --git a/tests/plugins/autoembed/manual/autoembed.html b/tests/plugins/autoembed/manual/autoembed.html new file mode 100644 index 00000000000..2a473b203d8 --- /dev/null +++ b/tests/plugins/autoembed/manual/autoembed.html @@ -0,0 +1,33 @@ +

Test URLs

+ + + +

Dat rich editor

+ + + + diff --git a/tests/plugins/autoembed/manual/autoembed.md b/tests/plugins/autoembed/manual/autoembed.md new file mode 100644 index 00000000000..e6f08f32b27 --- /dev/null +++ b/tests/plugins/autoembed/manual/autoembed.md @@ -0,0 +1,13 @@ +@bender-ui: collapsed +@bender-include: ../../embedbase/_helpers/tools.js +@bender-ckeditor-plugins: wysiwygarea,sourcearea,htmlwriter,entities,toolbar,elementspath,undo,clipboard,format,basicstyles,image2,embed,autolink,autoembed,link + +Play with the Auto Media Embed plugin. + +Things to check: + +* Breaking the link in two before it's embedded. +* Deleting the link before it's embedded. +* Other content changes. +* Pasting more complex content (only single links should be embedded). +* Undo/redo. Note: There should be two steps – one reverting autoembed and one reverting link paste. \ No newline at end of file diff --git a/tests/plugins/autolink/autolink.js b/tests/plugins/autolink/autolink.js new file mode 100644 index 00000000000..eeefba0ef4c --- /dev/null +++ b/tests/plugins/autolink/autolink.js @@ -0,0 +1,157 @@ +'use strict'; + +/* bender-tags: editor,unit */ +/* bender-ckeditor-plugins: autolink,clipboard */ +/* bender-include: ../clipboard/_helpers/pasting.js */ +/* global assertPasteEvent */ + +bender.editor = { + config: { + allowedContent: true, + pasteFilter: null + } +}; + +bender.test( { + 'test normal link': function() { + var pastedText = 'https://placekitten.com/g/180/300', + expected = '' + pastedText + ''; + + assertPasteEvent( this.editor, { dataValue: pastedText }, { dataValue: expected, type: 'html' } ); + }, + + 'test fake link': function() { + var pastedText = 'https//placekitten.com/g/190/300'; + + assertPasteEvent( this.editor, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } ); + }, + + 'test link with HTML tags': function() { + var pastedTexts = [ + 'https://
placekitten.com/g/200/301', + 'https://
placekitten.com/g/200/302', + 'https://
placekitten.com/g/200/303', + 'https://placekitten.com/g/200/303' + ]; + + var that = this; + pastedTexts.forEach( function( pastedText ) { + that.editor.once( 'paste', function( evt ) { + evt.cancel(); + + assert.areSame( -1, evt.data.dataValue.search( /' + pastedText + '', evt.data.dataValue ); + }, null, null, 900 ); + + that.editor.execCommand( 'paste', pastedText ); + } ); + }, + + 'test various invalid links': function() { + var pastedTexts = [ + 'https://placekitten.com/g/181/300.', + 'http://giphy.com?search', + 'https://www.google.pl,,,,', + 'http:///a' + ]; + + var that = this; + pastedTexts.forEach( function( pastedText ) { + that.editor.once( 'paste', function( evt ) { + evt.cancel(); + + assert.areSame( pastedText, evt.data.dataValue ); + }, null, null, 900 ); + + that.editor.execCommand( 'paste', pastedText ); + } ); + }, + + 'test pasting multiple links': function() { + var pastedText = 'http://en.wikipedia.org/wiki/Weasel http://en.wikipedia.org/wiki/Weasel'; + + assertPasteEvent( this.editor, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } ); + }, + + 'test pasting whole paragraph': function() { + var pastedText = + 'A multi-channel operating strategy' + + 'drives the enabler, while the enablers strategically embrace the game-changing, ' + + 'organic and cross-enterprise cultures.'; + + assertPasteEvent( this.editor, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } ); + }, + + 'test content that is a link': function() { + var pastedText = 'Weasel'; + + assertPasteEvent( this.editor, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } ); + }, + + 'test type is changed once a link is created': function() { + var pastedText = 'https://placekitten.com/g/180/300', + expected = '' + pastedText + ''; + + assertPasteEvent( this.editor, { dataValue: pastedText, type: 'text' }, { dataValue: expected, type: 'html' } ); + }, + + 'test type is not changed if link was not found': function() { + var pastedText = 'foo bar'; + + assertPasteEvent( this.editor, { dataValue: pastedText, type: 'text' }, { dataValue: pastedText, type: 'text' } ); + }, + + 'test internal paste is not autolinked': function() { + var editor = this.editor, + pastedText = 'https://foo.bar/g/185/310'; + + this.editor.once( 'paste', function( evt ) { + evt.data.dataTransfer.sourceEditor = editor; + }, null, null, 1 ); + + this.editor.once( 'paste', function( evt ) { + evt.cancel(); + + assert.areSame( pastedText, evt.data.dataValue ); + }, null, null, 900 ); + + this.editor.execCommand( 'paste', pastedText ); + } +} ); \ No newline at end of file From 640c1b0fb2c726d2873807ab80cb302e4af291df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Fri, 12 Jun 2015 17:58:27 +0200 Subject: [PATCH 2/8] Autoembed should not require embed plugin, because it should work with any embed plugin. --- plugins/autoembed/plugin.js | 2 +- tests/plugins/autoembed/autoembed.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/autoembed/plugin.js b/plugins/autoembed/plugin.js index 8342c81f011..d6565386983 100644 --- a/plugins/autoembed/plugin.js +++ b/plugins/autoembed/plugin.js @@ -7,7 +7,7 @@ ( function() { CKEDITOR.plugins.add( 'autoembed', { - requires: 'autolink,embed,undo', + requires: 'autolink,undo', init: function( editor ) { var currentId = 1; diff --git a/tests/plugins/autoembed/autoembed.js b/tests/plugins/autoembed/autoembed.js index 0291a4c41e2..cb35c00f2e3 100644 --- a/tests/plugins/autoembed/autoembed.js +++ b/tests/plugins/autoembed/autoembed.js @@ -1,5 +1,5 @@ /* bender-tags: editor,unit */ -/* bender-ckeditor-plugins: embedbase,autoembed,enterkey,undo */ +/* bender-ckeditor-plugins: embed,autoembed,enterkey,undo */ /* bender-include: ../embedbase/_helpers/tools.js, ../clipboard/_helpers/pasting.js */ /* global embedTools, assertPasteEvent */ From 0c4651b66d8790dbd4ea915a217f3622e9c1c439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Tue, 16 Jun 2015 10:35:28 +0200 Subject: [PATCH 3/8] Tests: Removed commented out tests and added single test for 2 step undo. --- tests/plugins/autoembed/autoembed.js | 100 ++++++++------------------- 1 file changed, 27 insertions(+), 73 deletions(-) diff --git a/tests/plugins/autoembed/autoembed.js b/tests/plugins/autoembed/autoembed.js index cb35c00f2e3..dbf69e7ffb0 100644 --- a/tests/plugins/autoembed/autoembed.js +++ b/tests/plugins/autoembed/autoembed.js @@ -153,88 +153,42 @@ bender.test( { assertPasteEvent( this.editor, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } ); }, - // 'test undo': function() { - // var bot = this.editorBot, - // pastedText = 'https://foo.bar/g/200/382'; + 'test 2 step undo': function() { + var bot = this.editorBot, + editor = bot.editor, + pastedText = 'https://foo.bar/g/200/382', + finalData = '

foo

bar

', + linkData = '

foo' + pastedText + 'bar

', + initialData = '

foobar

'; - // bot.setData( '', function() { - // this.editor.execCommand( 'paste', pastedText ); - // assert.areSame( '

' + pastedText + '

', bot.getData() ); - - // this.editor.execCommand( 'undo' ); - - // wait( function() { - // assert.areSame( '', bot.getData() ); - // }, 200 ); - // } ); - // }, - - // 'test undo after embed': function() { - // var bot = this.editorBot, - // pastedText = 'https://foo.bar/g/200/382', - // that = this; - - // bot.setData( '', function() { - // this.editor.execCommand( 'paste', pastedText ); - // assert.areSame( '

' + pastedText + '

', bot.getData() ); - - // wait( function() { - // this.editor.execCommand( 'undo' ); - - // // The "cke-auto-embed" attribute should not be present. - // assert.isInnerHtmlMatching( '

' + - // pastedText + '[]

', bender.tools.selection.getWithHtml( that.editor ) ); - // }, 200 ); - // } ); - // }, - - // 'test double undo': function() { - // var bot = this.editorBot, - // pastedText = 'https://foo.bar/g/210/382'; - - // bot.setData( '

', function() { - // this.editor.execCommand( 'paste', pastedText ); - // assert.areSame( '

' + pastedText + '

', bot.getData() ); - - // wait( function() { - // assert.areSame( '
', bot.getData() ); - - // this.editor.execCommand( 'undo' ); - - // assert.areSame( '

' + pastedText + '

', bot.getData() ); + bot.setData( '', function() { + editor.focus(); + bender.tools.selection.setWithHtml( editor, '

foo{}bar

' ); + editor.resetUndo(); - // this.editor.execCommand( 'undo' ); + editor.execCommand( 'paste', pastedText ); - // assert.areSame( '', bot.getData() ); - // }, 200 ); - // } ); - // }, + wait( function() { + assert.areSame( finalData, editor.getData(), 'start' ); - // 'test undo and redo': function() { - // var bot = this.editorBot, - // pastedText = 'https://foo.bar/g/400/382', - // that = this; + editor.execCommand( 'undo' ); + assert.areSame( linkData, editor.getData(), 'after 1st undo' ); - // bot.setData( '

', function() { - // this.editor.execCommand( 'paste', pastedText ); - // assert.areSame( '

' + pastedText + '

', bot.getData() ); + editor.execCommand( 'undo' ); + assert.areSame( initialData, editor.getData(), 'after 2nd undo' ); - // wait( function() { - // this.editor.execCommand( 'undo' ); - // assert.isInnerHtmlMatching( '

[]

', bender.tools.selection.getWithHtml( that.editor ) ); + assert.areSame( CKEDITOR.TRISTATE_DISABLED, editor.getCommand( 'undo' ).state, 'undo is disabled' ); - // this.editor.execCommand( 'redo' ); - // assert.isInnerHtmlMatching( [ '

' + - // pastedText + '[]

' ], bender.tools.selection.getWithHtml( that.editor ) ); + editor.execCommand( 'redo' ); + assert.areSame( linkData, editor.getData(), 'after 1st redo' ); - // // Embedding never really happened, so an additional redo step should do nothing. - // this.editor.execCommand( 'redo' ); - // assert.isInnerHtmlMatching( [ '

' + - // pastedText + '[]

' ], bender.tools.selection.getWithHtml( that.editor ) ); + editor.execCommand( 'redo' ); + assert.areSame( finalData, editor.getData(), 'after 2nd redo' ); - // }, 50 ); // User fired undo before the link was embedded. - // } ); - // }, + assert.areSame( CKEDITOR.TRISTATE_DISABLED, editor.getCommand( 'redo' ).state, 'redo is disabled' ); + }, 200 ); + } ); + }, 'test internal paste is not auto embedded - text URL': function() { var editor = this.editor, From 5b0177fed64b5b29ebecc6796b08164446029012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Tue, 16 Jun 2015 11:15:05 +0200 Subject: [PATCH 4/8] Tests: Be as much natural as possible. --- tests/plugins/autoembed/autoembed.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/plugins/autoembed/autoembed.js b/tests/plugins/autoembed/autoembed.js index dbf69e7ffb0..993023b9eb5 100644 --- a/tests/plugins/autoembed/autoembed.js +++ b/tests/plugins/autoembed/autoembed.js @@ -1,5 +1,5 @@ /* bender-tags: editor,unit */ -/* bender-ckeditor-plugins: embed,autoembed,enterkey,undo */ +/* bender-ckeditor-plugins: embed,autoembed,enterkey,undo,link */ /* bender-include: ../embedbase/_helpers/tools.js, ../clipboard/_helpers/pasting.js */ /* global embedTools, assertPasteEvent */ @@ -22,10 +22,7 @@ embedTools.mockJsonp( function() { } ); bender.editor = { - creator: 'inline', - config: { - allowedContent: true - } + creator: 'inline' }; bender.test( { From 416059b2ba7257ac0864cc03f83a1387665c321b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Tue, 16 Jun 2015 11:59:37 +0200 Subject: [PATCH 5/8] Implemented and used config.autoEmbed_widget. --- plugins/autoembed/plugin.js | 79 +++++++++++++- .../plugins/autoembed/getwidgetdefinition.js | 102 ++++++++++++++++++ tests/plugins/autoembed/manual/autoembed.html | 36 +++++-- tests/plugins/autoembed/manual/autoembed.md | 5 +- 4 files changed, 206 insertions(+), 16 deletions(-) create mode 100644 tests/plugins/autoembed/getwidgetdefinition.js diff --git a/plugins/autoembed/plugin.js b/plugins/autoembed/plugin.js index d6565386983..f27e186f4b0 100644 --- a/plugins/autoembed/plugin.js +++ b/plugins/autoembed/plugin.js @@ -51,10 +51,19 @@ return; } - // TODO Make this configurable (see http://dev.ckeditor.com/ticket/13214#comment:2). - var widgetDef = editor.widgets.registered.embed, + var href = anchor.data( 'cke-saved-href' ), + widgetDef = CKEDITOR.plugins.autoEmbed.getWidgetDefinition( editor, href ); + + if ( !widgetDef ) { + window.console && window.console.log( + '[CKEDITOR.plugins.autoEmbed] Incorrect config.autoEmbed_widget value. ' + + 'No widget definition found.' + ); + return; + } + // TODO Move this to a method in the widget plugin. - defaults = typeof widgetDef.defaults == 'function' ? widgetDef.defaults() : widgetDef.defaults, + var defaults = typeof widgetDef.defaults == 'function' ? widgetDef.defaults() : widgetDef.defaults, element = CKEDITOR.dom.element.createFromHtml( widgetDef.template.output( defaults ) ), instance, wrapper = editor.widgets.wrapElement( element, widgetDef.name ), @@ -68,7 +77,7 @@ return; } - instance.loadContent( anchor.data( 'cke-saved-href' ), { + instance.loadContent( href, { callback: function() { // DOM might be invalidated in the meantime, so find the anchor again. var anchor = editor.editable().findOne( 'a[data-cke-autoembed="' + id + '"]' ), @@ -92,4 +101,66 @@ editor.widgets.finalizeCreation( temp ); } } + + CKEDITOR.plugins.autoEmbed = { + /** + * Gets definition of a widget that should be used to automatically embed the specified link. + * + * This method uses value of the {@link CKEDITOR.config#autoEmbed_widget} option. + * + * @since 4.5.0 + * @param {CKEDITOR.editor} editor + * @param {String} url URL to be embedded. + * @returns {CKEDITOR.plugins.widget.definition/null} Definition of the widget to be used to embed the link. + */ + getWidgetDefinition: function( editor, url ) { + var opt = editor.config.autoEmbed_widget || 'embed,embedSemantic', + name, + widgets = editor.widgets.registered; + + if ( typeof opt == 'string' ) { + opt = opt.split( ',' ); + + while ( ( name = opt.shift() ) ) { + if ( widgets[ name ] ) { + return widgets[ name ]; + } + } + } else if ( typeof opt == 'function' ) { + return widgets[ opt( url ) ]; + } + + return null; + } + }; + + /** + * Specifies which widget to use to automatically embed a link. The default value + * of this option defines that either the [Embed](ckeditor.com/addon/embed) or + * [Semantic Embed](ckeditor.com/addon/embedsemantic) widgets will be used, depending on which is enabled. + * + * The general behavior: + * + * * If string (widget names separated by commas) is provided, then first of the listed widgets which is register + * will be used. For example, if `'foo,bar,bom'` is set and widgets `'bar'` and `'bom'` are registered, then `'bar'` + * will be used. + * * If a callback is specified, then it will be executed with the URL to be embedded and it should return a + * name of the widget to be used. This allows to use different embed widgets for different URLs. + * + * Example: + * + * // Defines that embedSemantic should be used (regardless of whether embed is defined). + * config.autoEmbed_widget = 'embedSemantic'; + * + * Using with custom embed widgets: + * + * config.autoEmbed_widget = 'customEmbed'; + * + * **Note:** Plugin names are always lower case, while widget names are not, so widget names do not have to equal plugin names. + * For example, there is the `embedsemantic` plugin and `embedSemantic` widget. + * + * @since 4.5.0 + * @cfg {String/Function} [autoEmbed_widget='embed,embedSemantic'] + * @member CKEDITOR.config + */ } )(); \ No newline at end of file diff --git a/tests/plugins/autoembed/getwidgetdefinition.js b/tests/plugins/autoembed/getwidgetdefinition.js new file mode 100644 index 00000000000..22f9cc17991 --- /dev/null +++ b/tests/plugins/autoembed/getwidgetdefinition.js @@ -0,0 +1,102 @@ +/* bender-tags: editor,unit */ +/* bender-ckeditor-plugins: autoembed,embed,embedsemantic,link */ +/* bender-include: ../embedbase/_helpers/tools.js */ + +/* global embedTools */ + +'use strict'; + +embedTools.mockJsonp( function( urlTemplate, urlParams, callback ) { + callback( { + 'url': decodeURIComponent( urlParams.url ), + 'type': 'rich', + 'version': '1.0', + 'html': '' + } ); +} ); + +bender.editor = { + creator: 'inline' +}; + +var WIDGET_FOO = {}, + WIDGET_BAR = {}; + +function mockEditor( opt ) { + return { + widgets: { + registered: { + foo: WIDGET_FOO, + bar: WIDGET_BAR + } + }, + + config: { + autoEmbed_widget: opt + } + }; +} + +bender.test( { + 'test getWidgetDefinition with default config': function() { + assert.areSame( this.editor.widgets.registered.embed, CKEDITOR.plugins.autoEmbed.getWidgetDefinition( this.editor, 'url' ) ); + }, + + 'test getWidgetDefinition with default config and no embed widget': function() { + var EMBED_SEMANTIC = {}, + editorMock = { + widgets: { + registered: { + foo: WIDGET_FOO, + embedSemantic: EMBED_SEMANTIC + } + }, + config: {} // No value defined, so default should be used. + }; + + assert.areSame( EMBED_SEMANTIC, CKEDITOR.plugins.autoEmbed.getWidgetDefinition( editorMock, 'url' ) ); + }, + + 'test getWidgetDefinition with string config - first used': function() { + var editorMock = mockEditor( 'foo,embed' ); + + assert.areSame( WIDGET_FOO, CKEDITOR.plugins.autoEmbed.getWidgetDefinition( editorMock, 'url' ) ); + }, + + 'test getWidgetDefinition with string config - first existing used': function() { + var editorMock = mockEditor( 'xxx,yyy,bar,foo' ); + + assert.areSame( WIDGET_BAR, CKEDITOR.plugins.autoEmbed.getWidgetDefinition( editorMock, 'url' ) ); + }, + + 'test getWidgetDefinition with string config - null if none found': function() { + var editorMock = mockEditor( 'xxx,yyy' ); + + assert.isNull( CKEDITOR.plugins.autoEmbed.getWidgetDefinition( editorMock, 'url' ) ); + }, + + 'test getWidgetDefinition with function config': function() { + var editorMock = mockEditor( function( url ) { + assert.areSame( 'http://foo.bar/bom', url, 'called with the URL' ); + return 'bar'; + } ); + + assert.areSame( WIDGET_BAR, CKEDITOR.plugins.autoEmbed.getWidgetDefinition( editorMock, 'http://foo.bar/bom' ) ); + }, + + 'test getWidgetDefinition is used by the plugin': function() { + var editor = this.editor, + mock = sinon.stub( CKEDITOR.plugins.autoEmbed, 'getWidgetDefinition' ).returns( editor.widgets.registered.embedSemantic ); + + bender.tools.selection.setWithHtml( editor, '

foo{}bar

' ); + editor.execCommand( 'paste', 'http://foo.bar/100' ); + + wait( function() { + mock.restore(); + + assert.areSame( '

foo

http://foo.bar/100

bar

', editor.getData(), 'embedsemantic was used' ); + assert.isTrue( mock.calledOnce, 'getWidgetDefinition was called once' ); + assert.areSame( 'http://foo.bar/100', mock.args[ 0 ][ 1 ], 'getWidgetDefinition was called with the URL being embedded' ); + }, 200 ); + } +} ); \ No newline at end of file diff --git a/tests/plugins/autoembed/manual/autoembed.html b/tests/plugins/autoembed/manual/autoembed.html index 2a473b203d8..9f52542c7e8 100644 --- a/tests/plugins/autoembed/manual/autoembed.html +++ b/tests/plugins/autoembed/manual/autoembed.html @@ -12,7 +12,7 @@
  • -

    Dat rich editor

    +

    Dat rich editor – embed

    +

    Dat rich editor – embedsemantic

    + + + diff --git a/tests/plugins/autoembed/manual/autoembed.md b/tests/plugins/autoembed/manual/autoembed.md index e6f08f32b27..248dd52e338 100644 --- a/tests/plugins/autoembed/manual/autoembed.md +++ b/tests/plugins/autoembed/manual/autoembed.md @@ -1,6 +1,6 @@ @bender-ui: collapsed @bender-include: ../../embedbase/_helpers/tools.js -@bender-ckeditor-plugins: wysiwygarea,sourcearea,htmlwriter,entities,toolbar,elementspath,undo,clipboard,format,basicstyles,image2,embed,autolink,autoembed,link +@bender-ckeditor-plugins: wysiwygarea,sourcearea,htmlwriter,entities,toolbar,elementspath,undo,clipboard,format,basicstyles,autolink,autoembed,link Play with the Auto Media Embed plugin. @@ -10,4 +10,5 @@ Things to check: * Deleting the link before it's embedded. * Other content changes. * Pasting more complex content (only single links should be embedded). -* Undo/redo. Note: There should be two steps – one reverting autoembed and one reverting link paste. \ No newline at end of file +* Undo/redo. Note: There should be two steps – one reverting autoembed and one reverting link paste. +* That in the 1st editor `embed` is used and in the 2nd editor `embedsemantic` (check the data). \ No newline at end of file From 5fa06b6faa2872f60bc57639ad753723e2bb307c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Tue, 16 Jun 2015 13:30:14 +0200 Subject: [PATCH 6/8] Improved a comment. --- plugins/autoembed/plugin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/autoembed/plugin.js b/plugins/autoembed/plugin.js index f27e186f4b0..5367964b877 100644 --- a/plugins/autoembed/plugin.js +++ b/plugins/autoembed/plugin.js @@ -62,7 +62,7 @@ return; } - // TODO Move this to a method in the widget plugin. + // TODO Move this to a method in the widget plugin. #13408 var defaults = typeof widgetDef.defaults == 'function' ? widgetDef.defaults() : widgetDef.defaults, element = CKEDITOR.dom.element.createFromHtml( widgetDef.template.output( defaults ) ), instance, From e37be00b800614e50d60d7cb69a4b54383557b6f Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 17 Jun 2015 10:44:43 +0200 Subject: [PATCH 7/8] Minor fixes to the doc strings. --- plugins/autoembed/plugin.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/autoembed/plugin.js b/plugins/autoembed/plugin.js index 5367964b877..818437fa12d 100644 --- a/plugins/autoembed/plugin.js +++ b/plugins/autoembed/plugin.js @@ -141,11 +141,11 @@ * * The general behavior: * - * * If string (widget names separated by commas) is provided, then first of the listed widgets which is register + * * If string (widget names separated by commas) is provided, then first of the listed widgets which is registered * will be used. For example, if `'foo,bar,bom'` is set and widgets `'bar'` and `'bom'` are registered, then `'bar'` * will be used. * * If a callback is specified, then it will be executed with the URL to be embedded and it should return a - * name of the widget to be used. This allows to use different embed widgets for different URLs. + * name of the widget to be used. It allows to use different embed widgets for different URLs. * * Example: * From cc0a9ae037c641e6fd74747e1b2b3065f036a533 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 17 Jun 2015 10:53:01 +0200 Subject: [PATCH 8/8] Changelog entry. --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 0935c7fb1b0..c6a75f59181 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ New Features: * [#13215](http://dev.ckeditor.com/ticket/13215): Added ability to cancel fetching a resource by the Embed plugins. * [#13213](http://dev.ckeditor.com/ticket/13213): Added [`dialog#setState()`](http://docs.ckeditor.com/#!/api/CKEDITOR.dialog-method-setState) method and used in in Embed dialog to indicate that a resource is being loaded. * [#13337](http://dev.ckeditor.com/ticket/13337): Added the [`repository.onWidget()`](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget.repository-method-onWidget) method – a convenient way to listen to [widget](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget) events through the [repository](http://docs.ckeditor.com/#!/api/CKEDITOR.plugins.widget.repository). +* [#13214](http://dev.ckeditor.com/ticket/13214): Added support for pasting links that convert into embeddable resources on the fly. Fixed Issues: