diff --git a/.eslintrc.js b/.eslintrc.js index ec4f4582126..632a1f71668 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,6 +16,7 @@ module.exports = { ], rules: { 'ckeditor5-rules/ckeditor-imports': 'error', + 'ckeditor5-rules/prevent-license-key-leak': 'error', 'ckeditor5-rules/license-header': [ 'error', { headerLines: [ '/**', diff --git a/docs/_snippets/examples/balloon-block-editor.js b/docs/_snippets/examples/balloon-block-editor.js index 424f5999874..23e20128dd6 100644 --- a/docs/_snippets/examples/balloon-block-editor.js +++ b/docs/_snippets/examples/balloon-block-editor.js @@ -23,7 +23,8 @@ BalloonEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/examples/balloon-editor.js b/docs/_snippets/examples/balloon-editor.js index 50a32be352f..6797b2fe070 100644 --- a/docs/_snippets/examples/balloon-editor.js +++ b/docs/_snippets/examples/balloon-editor.js @@ -25,7 +25,8 @@ BalloonEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/examples/bottom-toolbar-editor.js b/docs/_snippets/examples/bottom-toolbar-editor.js index a1056310114..66ddac4b825 100644 --- a/docs/_snippets/examples/bottom-toolbar-editor.js +++ b/docs/_snippets/examples/bottom-toolbar-editor.js @@ -202,8 +202,8 @@ DecoupledEditor 'mergeTableCells' ] }, - - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/examples/classic-editor-short.js b/docs/_snippets/examples/classic-editor-short.js index af93df3d7e9..38b57b927cc 100644 --- a/docs/_snippets/examples/classic-editor-short.js +++ b/docs/_snippets/examples/classic-editor-short.js @@ -14,7 +14,8 @@ ClassicEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/examples/classic-editor.js b/docs/_snippets/examples/classic-editor.js index 74f878bb614..4a18358e547 100644 --- a/docs/_snippets/examples/classic-editor.js +++ b/docs/_snippets/examples/classic-editor.js @@ -23,7 +23,8 @@ ClassicEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/examples/document-editor.js b/docs/_snippets/examples/document-editor.js index 26e3c2fdd19..6c4c754266d 100644 --- a/docs/_snippets/examples/document-editor.js +++ b/docs/_snippets/examples/document-editor.js @@ -29,7 +29,8 @@ DecoupledEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { const toolbarContainer = document.querySelector( '.document-editor__toolbar' ); diff --git a/docs/_snippets/examples/inline-editor.js b/docs/_snippets/examples/inline-editor.js index 05cf963c503..2abccb3c687 100644 --- a/docs/_snippets/examples/inline-editor.js +++ b/docs/_snippets/examples/inline-editor.js @@ -26,7 +26,8 @@ Array.from( inlineInjectElements ).forEach( inlineElement => { '|', 'bulletedList', 'numberedList', 'outdent', 'indent' ] }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' }; if ( inlineElement.tagName.toLowerCase() == 'header' ) { diff --git a/docs/_snippets/examples/multi-root-editor.js b/docs/_snippets/examples/multi-root-editor.js index 0b64f81bbed..23597faf1db 100644 --- a/docs/_snippets/examples/multi-root-editor.js +++ b/docs/_snippets/examples/multi-root-editor.js @@ -28,7 +28,8 @@ MultiRootEditor '|', 'link', 'uploadImage', 'insertTable', 'mediaEmbed', '|', 'bulletedList', 'numberedList', 'outdent', 'indent' ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { diff --git a/docs/_snippets/features/image-upload.js b/docs/_snippets/features/image-upload.js index db55bbcce79..f6d47761cee 100644 --- a/docs/_snippets/features/image-upload.js +++ b/docs/_snippets/features/image-upload.js @@ -61,7 +61,8 @@ ClassicEditor tokenUrl: TOKEN_URL, forceDemoLabel: true, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/features/mathtype.js b/docs/_snippets/features/mathtype.js index 36c9cdfe79b..64eca6f9eba 100644 --- a/docs/_snippets/features/mathtype.js +++ b/docs/_snippets/features/mathtype.js @@ -59,7 +59,8 @@ ClassicEditor table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { diff --git a/docs/_snippets/features/mermaid.js b/docs/_snippets/features/mermaid.js index e7753ae17c6..2b5dbe0f3d2 100644 --- a/docs/_snippets/features/mermaid.js +++ b/docs/_snippets/features/mermaid.js @@ -39,7 +39,8 @@ ClassicEditor top: window.getViewportTopOffsetConfig() } }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/features/placeholder-custom.js b/docs/_snippets/features/placeholder-custom.js index fa42274a47d..0787b9ac505 100644 --- a/docs/_snippets/features/placeholder-custom.js +++ b/docs/_snippets/features/placeholder-custom.js @@ -32,7 +32,8 @@ ClassicEditor top: window.getViewportTopOffsetConfig() } }, - placeholder: 'Type some content here!' + placeholder: 'Type some content here!', + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/features/placeholder.js b/docs/_snippets/features/placeholder.js index 193ae891fc4..a8e2a3e1e3e 100644 --- a/docs/_snippets/features/placeholder.js +++ b/docs/_snippets/features/placeholder.js @@ -32,7 +32,8 @@ ClassicEditor top: window.getViewportTopOffsetConfig() } }, - placeholder: 'Type some content here!' + placeholder: 'Type some content here!', + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/features/read-only-hide-toolbar.js b/docs/_snippets/features/read-only-hide-toolbar.js index 9dacfa3b14f..483475ad291 100644 --- a/docs/_snippets/features/read-only-hide-toolbar.js +++ b/docs/_snippets/features/read-only-hide-toolbar.js @@ -35,7 +35,8 @@ ClassicEditor tokenUrl: TOKEN_URL, forceDemoLabel: true, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/features/read-only.js b/docs/_snippets/features/read-only.js index 7bfc00837cd..37abee60596 100644 --- a/docs/_snippets/features/read-only.js +++ b/docs/_snippets/features/read-only.js @@ -57,7 +57,8 @@ ClassicEditor exportWord: { fileName: 'export-word-demo.docx', tokenUrl: false - } + }, + licenseKey: 'GPL' } ) .then( editor => { const button = document.querySelector( '#snippet-read-only-toggle' ); diff --git a/docs/_snippets/features/ui-language-content.js b/docs/_snippets/features/ui-language-content.js index 9045e07407b..e2f38daf155 100644 --- a/docs/_snippets/features/ui-language-content.js +++ b/docs/_snippets/features/ui-language-content.js @@ -38,7 +38,8 @@ ClassicEditor tokenUrl: TOKEN_URL, forceDemoLabel: true, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/features/ui-language-rtl.js b/docs/_snippets/features/ui-language-rtl.js index 07cb13c0c29..4eea51d3f90 100644 --- a/docs/_snippets/features/ui-language-rtl.js +++ b/docs/_snippets/features/ui-language-rtl.js @@ -36,7 +36,8 @@ ClassicEditor tokenUrl: TOKEN_URL, forceDemoLabel: true, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/features/ui-language.js b/docs/_snippets/features/ui-language.js index ba09fe6b63a..edd7f8f26c9 100644 --- a/docs/_snippets/features/ui-language.js +++ b/docs/_snippets/features/ui-language.js @@ -36,7 +36,8 @@ ClassicEditor tokenUrl: TOKEN_URL, forceDemoLabel: true, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/features/update-placeholder.js b/docs/_snippets/features/update-placeholder.js index 19f6a018ee7..5c5c19a348e 100644 --- a/docs/_snippets/features/update-placeholder.js +++ b/docs/_snippets/features/update-placeholder.js @@ -26,7 +26,8 @@ ClassicEditor top: window.getViewportTopOffsetConfig() } }, - placeholder: 'Type some content here!' + placeholder: 'Type some content here!', + licenseKey: 'GPL' } ) .then( editor => { const button = document.getElementById( 'update-placeholder-button' ); diff --git a/docs/_snippets/features/wproofreader.js b/docs/_snippets/features/wproofreader.js index f068ece3779..0b30c5a929f 100644 --- a/docs/_snippets/features/wproofreader.js +++ b/docs/_snippets/features/wproofreader.js @@ -52,7 +52,8 @@ ClassicEditor }, table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/framework/development-tools/inspector.js b/docs/_snippets/framework/development-tools/inspector.js index fe70413282c..2d4d93575bf 100644 --- a/docs/_snippets/framework/development-tools/inspector.js +++ b/docs/_snippets/framework/development-tools/inspector.js @@ -16,7 +16,8 @@ ClassicEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/framework/tutorials/block-widget.js b/docs/_snippets/framework/tutorials/block-widget.js index 8093e8fa425..e84ab66aff8 100644 --- a/docs/_snippets/framework/tutorials/block-widget.js +++ b/docs/_snippets/framework/tutorials/block-widget.js @@ -233,7 +233,8 @@ ClassicEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { console.log( 'Editor was initialized', editor ); diff --git a/docs/_snippets/framework/tutorials/external-data-widget.js b/docs/_snippets/framework/tutorials/external-data-widget.js index d4b0b867295..178e8eaf1d4 100644 --- a/docs/_snippets/framework/tutorials/external-data-widget.js +++ b/docs/_snippets/framework/tutorials/external-data-widget.js @@ -194,7 +194,8 @@ class ExternalDataWidgetEditing extends Plugin { ClassicEditor .create( document.querySelector( '#snippet-external-data-widget' ), { plugins: [ Essentials, Paragraph, Heading, List, Bold, Italic, ExternalDataWidget ], - toolbar: [ 'undo', 'redo', '|', 'external', '|', 'heading', '|', 'bold', 'italic', '|', 'numberedList', 'bulletedList' ] + toolbar: [ 'undo', 'redo', '|', 'external', '|', 'heading', '|', 'bold', 'italic', '|', 'numberedList', 'bulletedList' ], + licenseKey: 'GPL' } ) .then( editor => { console.log( 'Editor was initialized', editor ); diff --git a/docs/_snippets/framework/tutorials/inline-widget.js b/docs/_snippets/framework/tutorials/inline-widget.js index bbba89f13cb..52615ad3648 100644 --- a/docs/_snippets/framework/tutorials/inline-widget.js +++ b/docs/_snippets/framework/tutorials/inline-widget.js @@ -221,7 +221,8 @@ ClassicEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { console.log( 'Editor was initialized', editor ); diff --git a/docs/_snippets/framework/tutorials/using-react-in-widget.html b/docs/_snippets/framework/tutorials/using-react-in-widget.html index 8b6c8a7c7ee..f5c8692a496 100644 --- a/docs/_snippets/framework/tutorials/using-react-in-widget.html +++ b/docs/_snippets/framework/tutorials/using-react-in-widget.html @@ -293,7 +293,8 @@

Other deals

); } - } + }, + licenseKey: 'GPL' }; this.handleEditorDataChange = this.handleEditorDataChange.bind( this ); diff --git a/docs/_snippets/framework/ui/ui-button.js b/docs/_snippets/framework/ui/ui-button.js index 4f410db35fc..df644dfadf1 100644 --- a/docs/_snippets/framework/ui/ui-button.js +++ b/docs/_snippets/framework/ui/ui-button.js @@ -68,7 +68,8 @@ document.querySelector( '.ui-button' ).append( toolbarButtons.element ); ClassicEditor .create( document.querySelector( '#ui-button-editor' ), { - plugins: [ Essentials ] + plugins: [ Essentials ], + licenseKey: 'GPL' } ) .then( editor => { this.tooltipManager = new TooltipManager( editor ); diff --git a/docs/_snippets/framework/ui/ui-dialog.js b/docs/_snippets/framework/ui/ui-dialog.js index 0284f69f9e7..8b990b5dc51 100644 --- a/docs/_snippets/framework/ui/ui-dialog.js +++ b/docs/_snippets/framework/ui/ui-dialog.js @@ -74,7 +74,8 @@ class MinimalisticDialog extends Plugin { ClassicEditor .create( document.querySelector( '#ui-dialog-editor' ), { plugins: [ Essentials, Paragraph, Bold, Italic, Underline, MinimalisticDialog, Dialog ], - toolbar: [ 'bold', 'italic', 'underline', '|', 'showDialog' ] + toolbar: [ 'bold', 'italic', 'underline', '|', 'showDialog' ], + licenseKey: 'GPL' } ) .then( editor => { window.attachTourBalloon( { diff --git a/docs/_snippets/framework/ui/ui-modal.js b/docs/_snippets/framework/ui/ui-modal.js index 0d8a101ba0d..097212c956a 100644 --- a/docs/_snippets/framework/ui/ui-modal.js +++ b/docs/_snippets/framework/ui/ui-modal.js @@ -75,7 +75,8 @@ class MinimalisticModal extends Plugin { ClassicEditor .create( document.querySelector( '#ui-modal-editor' ), { plugins: [ Essentials, Paragraph, Bold, Italic, Underline, MinimalisticModal, Dialog ], - toolbar: [ 'bold', 'italic', 'underline', '|', 'showModal' ] + toolbar: [ 'bold', 'italic', 'underline', '|', 'showModal' ], + licenseKey: 'GPL' } ) .then( editor => { window.attachTourBalloon( { diff --git a/docs/_snippets/installation/getting-and-setting-data/manualsave.js b/docs/_snippets/installation/getting-and-setting-data/manualsave.js index 1f41158602f..bb516d89869 100644 --- a/docs/_snippets/installation/getting-and-setting-data/manualsave.js +++ b/docs/_snippets/installation/getting-and-setting-data/manualsave.js @@ -21,7 +21,8 @@ ClassicEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/installation/setup/blocktoolbar.js b/docs/_snippets/installation/setup/blocktoolbar.js index 90534cfd238..a8b15715e04 100644 --- a/docs/_snippets/installation/setup/blocktoolbar.js +++ b/docs/_snippets/installation/setup/blocktoolbar.js @@ -32,7 +32,8 @@ ClassicEditor '|', 'outdent', 'indent' ], - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editorBasic = editor; diff --git a/docs/_snippets/installation/setup/toolbar-basic.js b/docs/_snippets/installation/setup/toolbar-basic.js index dd0153c1ab2..203826bf0aa 100644 --- a/docs/_snippets/installation/setup/toolbar-basic.js +++ b/docs/_snippets/installation/setup/toolbar-basic.js @@ -33,7 +33,8 @@ ClassicEditor tokenUrl: TOKEN_URL, forceDemoLabel: true, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/installation/setup/toolbar-breakpoint.js b/docs/_snippets/installation/setup/toolbar-breakpoint.js index 9083e7d2df4..3c09e4f6559 100644 --- a/docs/_snippets/installation/setup/toolbar-breakpoint.js +++ b/docs/_snippets/installation/setup/toolbar-breakpoint.js @@ -61,7 +61,8 @@ ClassicEditor { model: 'heading4', view: 'h5', title: 'Heading 4', class: 'ck-heading_heading4' } ] }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/installation/setup/toolbar-grouping.js b/docs/_snippets/installation/setup/toolbar-grouping.js index fa4c947ecc4..a4021f32e33 100644 --- a/docs/_snippets/installation/setup/toolbar-grouping.js +++ b/docs/_snippets/installation/setup/toolbar-grouping.js @@ -58,7 +58,8 @@ ClassicEditor { model: 'heading4', view: 'h5', title: 'Heading 4', class: 'ck-heading_heading4' } ] }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/installation/setup/toolbar-nested-icon.js b/docs/_snippets/installation/setup/toolbar-nested-icon.js index 6934b9eec7b..6bf5d274f46 100644 --- a/docs/_snippets/installation/setup/toolbar-nested-icon.js +++ b/docs/_snippets/installation/setup/toolbar-nested-icon.js @@ -56,7 +56,8 @@ ClassicEditor tokenUrl: TOKEN_URL, forceDemoLabel: true, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/installation/setup/toolbar-nested-label.js b/docs/_snippets/installation/setup/toolbar-nested-label.js index 9e210f9701b..6749e6284d9 100644 --- a/docs/_snippets/installation/setup/toolbar-nested-label.js +++ b/docs/_snippets/installation/setup/toolbar-nested-label.js @@ -45,7 +45,8 @@ ClassicEditor tokenUrl: TOKEN_URL, forceDemoLabel: true, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/installation/setup/toolbar-nested-simple.js b/docs/_snippets/installation/setup/toolbar-nested-simple.js index f522168d3af..a09f9e843aa 100644 --- a/docs/_snippets/installation/setup/toolbar-nested-simple.js +++ b/docs/_snippets/installation/setup/toolbar-nested-simple.js @@ -48,7 +48,8 @@ ClassicEditor tokenUrl: TOKEN_URL, forceDemoLabel: true, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/installation/setup/toolbar-nested-tooltip.js b/docs/_snippets/installation/setup/toolbar-nested-tooltip.js index e1dc9b1b11a..1a853bbcb16 100644 --- a/docs/_snippets/installation/setup/toolbar-nested-tooltip.js +++ b/docs/_snippets/installation/setup/toolbar-nested-tooltip.js @@ -39,7 +39,8 @@ ClassicEditor tokenUrl: TOKEN_URL, forceDemoLabel: true, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/installation/setup/toolbar-separator.js b/docs/_snippets/installation/setup/toolbar-separator.js index d055fafd51d..7f405613511 100644 --- a/docs/_snippets/installation/setup/toolbar-separator.js +++ b/docs/_snippets/installation/setup/toolbar-separator.js @@ -33,7 +33,8 @@ ClassicEditor tokenUrl: TOKEN_URL, forceDemoLabel: true, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/installation/setup/toolbar-wrapping.js b/docs/_snippets/installation/setup/toolbar-wrapping.js index 4d3315c3909..75d083eba36 100644 --- a/docs/_snippets/installation/setup/toolbar-wrapping.js +++ b/docs/_snippets/installation/setup/toolbar-wrapping.js @@ -63,7 +63,8 @@ ClassicEditor table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties' ] }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/support/managing-ckeditor-logo-position.js b/docs/_snippets/support/managing-ckeditor-logo-position.js index 0f448d88e00..c816c079335 100644 --- a/docs/_snippets/support/managing-ckeditor-logo-position.js +++ b/docs/_snippets/support/managing-ckeditor-logo-position.js @@ -28,7 +28,8 @@ ClassicEditor side: 'left', label: 'This is' } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/support/managing-ckeditor-logo-styling.js b/docs/_snippets/support/managing-ckeditor-logo-styling.js index df0e0c8f651..33501bb14ef 100644 --- a/docs/_snippets/support/managing-ckeditor-logo-styling.js +++ b/docs/_snippets/support/managing-ckeditor-logo-styling.js @@ -23,7 +23,8 @@ ClassicEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/tutorials/abbreviation-level-1.js b/docs/_snippets/tutorials/abbreviation-level-1.js index 29ca0a9fa80..f00957ce03d 100644 --- a/docs/_snippets/tutorials/abbreviation-level-1.js +++ b/docs/_snippets/tutorials/abbreviation-level-1.js @@ -97,7 +97,8 @@ ClassicEditor } }, plugins: [ Essentials, Bold, Italic, Heading, List, Paragraph, Abbreviation ], - toolbar: [ 'heading', '|', 'bold', 'italic', 'numberedList', 'bulletedList', '|', 'abbreviation' ] + toolbar: [ 'heading', '|', 'bold', 'italic', 'numberedList', 'bulletedList', '|', 'abbreviation' ], + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/tutorials/abbreviation-level-2.js b/docs/_snippets/tutorials/abbreviation-level-2.js index 33d54588d0d..668157c08cc 100644 --- a/docs/_snippets/tutorials/abbreviation-level-2.js +++ b/docs/_snippets/tutorials/abbreviation-level-2.js @@ -169,7 +169,8 @@ ClassicEditor } }, plugins: [ Essentials, Bold, Italic, Heading, List, Paragraph, Abbreviation ], - toolbar: [ 'heading', '|', 'bold', 'italic', 'numberedList', 'bulletedList', '|', 'abbreviation' ] + toolbar: [ 'heading', '|', 'bold', 'italic', 'numberedList', 'bulletedList', '|', 'abbreviation' ], + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/tutorials/abbreviation-level-3.js b/docs/_snippets/tutorials/abbreviation-level-3.js index 1be7f3937f7..fe09f88d837 100644 --- a/docs/_snippets/tutorials/abbreviation-level-3.js +++ b/docs/_snippets/tutorials/abbreviation-level-3.js @@ -321,7 +321,8 @@ ClassicEditor } }, plugins: [ Essentials, Bold, Italic, Heading, List, Paragraph, Abbreviation ], - toolbar: [ 'heading', '|', 'bold', 'italic', 'numberedList', 'bulletedList', '|', 'abbreviation' ] + toolbar: [ 'heading', '|', 'bold', 'italic', 'numberedList', 'bulletedList', '|', 'abbreviation' ], + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/_snippets/tutorials/timestamp-plugin.js b/docs/_snippets/tutorials/timestamp-plugin.js index f5072e337f4..84234415428 100644 --- a/docs/_snippets/tutorials/timestamp-plugin.js +++ b/docs/_snippets/tutorials/timestamp-plugin.js @@ -51,7 +51,8 @@ ClassicEditor } }, plugins: [ Essentials, Bold, Italic, Heading, List, Paragraph, Timestamp ], - toolbar: [ 'heading', '|', 'bold', 'italic', 'numberedList', 'bulletedList', '|', 'timestamp' ] + toolbar: [ 'heading', '|', 'bold', 'italic', 'numberedList', 'bulletedList', '|', 'timestamp' ], + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/docs/examples/builds/multi-root-editor.md b/docs/examples/builds/multi-root-editor.md index cfa2dbfc8f8..6c11d08b1de 100644 --- a/docs/examples/builds/multi-root-editor.md +++ b/docs/examples/builds/multi-root-editor.md @@ -17,7 +17,7 @@ The main difference between using a multi-root editor and using multiple separat ## Editor example configuration -Check out the {@link getting-started/quick-start Quick start} guide to learn more about implementing this kind of editor. You will find implementation steps there. You can see this example editor’s code below. +Check out the {@link getting-started/integrations-cdn/quick-start Quick start} guide to learn more about implementing this kind of editor. You will find implementation steps there. You can see this example editor’s code below.
View editor configuration script @@ -49,6 +49,7 @@ MultiRootEditor }, // Editor configration: { + licenseKey: 'GPL', // Or ''. plugins: [ Essentials, Heading, diff --git a/docs/examples/custom/bottom-toolbar-editor.md b/docs/examples/custom/bottom-toolbar-editor.md index 05748c2b19a..89e1e70c46c 100644 --- a/docs/examples/custom/bottom-toolbar-editor.md +++ b/docs/examples/custom/bottom-toolbar-editor.md @@ -160,6 +160,7 @@ class FormattingOptions extends Plugin { DecoupledEditor .create( document.querySelector( '#editor-content' ), { + licenseKey: 'GPL', // Or ''. plugins: [ Alignment, Autoformat, @@ -261,7 +262,6 @@ DecoupledEditor // Provide correct configuration values to use it. tokenUrl: 'https://example.com/cs-token-endpoint', uploadUrl: 'https://your-organization-id.cke-cs.com/easyimage/upload/' - // Read more about Easy Image - https://ckeditor.com/docs/ckeditor5/latest/features/images/image-upload/easy-image.html. // For other image upload methods see the guide - https://ckeditor.com/docs/ckeditor5/latest/features/images/image-upload/image-upload.html. }, } ) diff --git a/docs/features/accessibility.md b/docs/features/accessibility.md index 319e6e47073..be4754c314e 100644 --- a/docs/features/accessibility.md +++ b/docs/features/accessibility.md @@ -166,7 +166,7 @@ These keyboard shortcuts allow for quick access to content editing features. -#### Keystrokes for interacting with annotation threads (e.g. comments, track changes suggestions) +#### Keystrokes for interacting with annotation threads (such as comments or track changes suggestions) diff --git a/docs/features/editor-placeholder.md b/docs/features/editor-placeholder.md index 176cd729f04..4f888150537 100644 --- a/docs/features/editor-placeholder.md +++ b/docs/features/editor-placeholder.md @@ -36,6 +36,7 @@ import { ClassicEditor, Essentials } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Essentials, /* ... */ ], } ) .then( editor => { @@ -55,11 +56,9 @@ You can use the {@link module:core/editor/editorconfig~EditorConfig#placeholder * to override the `placeholder` text of a ` + + + +``` + +### Step 2: Replace CKEditor 5 imports with `window.CKEDITOR` + +Since the CKEditor 5 script is now included via the CDN, you can access the `ClassicEditor` object directly in your JavaScript file using the `window.CKEDITOR` global variable. It means that `import` statements are no longer needed and you can remove them from your JavaScript files. Here is an example of migrating the CKEditor 5 initialization code: + +**Before:** + +```javascript +import { ClassicEditor } from 'ckeditor5'; +import { AIAdapter, /* ... other imports */ } from 'ckeditor5-premium-features'; + +ClassicEditor + .create( document.querySelector('#editor'), { + licenseKey: '', // Or 'GPL'. + // ... other configuration + } ) + .catch( error => { + console.error(error); + } ); +``` + +**After:** + +```javascript +const { ClassicEditor } = window.CKEDITOR; +const { AIAdapter, /* ... other imports */ } = window.CKEDITOR_PREMIUM_FEATURES; + +ClassicEditor + .create( document.querySelector('#editor'), { + licenseKey: '', // Or 'GPL'. + // ... other configuration + } ) + .catch( error => { + console.error(error); + } ); +``` + +## Using lazy injection of CKEditor 5 + +If you prefer to automatically inject the CKEditor 5 script into your HTML file, you can migrate your project using the `@ckeditor/ckeditor5-integrations-common` package. This package provides a `loadCKEditorCloud` function that automatically injects the CKEditor 5 scripts and styles into your HTML file. It may be useful when your project uses a bundler like Webpack or Rollup and you cannot modify your head section directly. + +### Step 1: Install the `@ckeditor/ckeditor5-integrations-common` Package + +First, install the `@ckeditor/ckeditor5-integrations-common` package using the following command: + +```bash +npm install @ckeditor/ckeditor5-integrations-common +``` + +### Step 2: Replace CKEditor 5 Imports + +If you have any CKEditor 5 imports in your JavaScript files, remove them. For example, remove lines like: + +```javascript +import { ClassicEditor, ... } from 'ckeditor5'; +import { AIAdapter, ... } from 'ckeditor5-premium-features'; +``` + +Next, update your JavaScript file to use the `loadCKEditorCloud` function from the `@ckeditor/ckeditor5-integrations-common` package. Here is an example of migrating the CKEditor 5 initialization code: + +**Before:** + +```javascript +import { ClassicEditor } from 'ckeditor5'; + +ClassicEditor + .create( document.querySelector('#editor') ) + .catch( error => { + console.error(error); + } ); +``` + +**After:** + +```javascript +import { loadCKEditorCloud } from '@ckeditor/ckeditor5-integrations-common'; + +const { ClassicEditor } = await loadCKEditorCloud( { + version: '{@var ckeditor5-version}', +} ); +``` + +## Conclusion + +Following these steps, you successfully migrated CKEditor 5 from an NPM-based installation to a CDN-based installation using Vanilla JS. This approach simplifies the setup process and can help improve the performance of your application by reducing the bundle size. diff --git a/docs/updating/migration-to-cdn/vuejs-v3.md b/docs/updating/migration-to-cdn/vuejs-v3.md new file mode 100644 index 00000000000..889017ab451 --- /dev/null +++ b/docs/updating/migration-to-cdn/vuejs-v3.md @@ -0,0 +1,189 @@ +--- +menu-title: Vue 3+ +meta-title: Vue CKEditor 5 - migrate integration from npm to CDN | CKEditor 5 documentation +meta-description: Migrate Vue 3+ CKEditor 5 integration from npm to CDN in a few simple steps. Learn how to install Vue 3+ CKEditor 5 integration in your project using the CDN. +category: migrations +order: 40 +--- + +# Migrating Vue 3+ CKEditor 5 integration from npm to CDN + +This guide will help you migrate Vue 3 CKEditor 5 integration from an NPM-based installation to a CDN-based one. + +## Prerequisites + +Remove the existing CKEditor 5 packages from your project. If you are using the NPM-based installation, you can remove it by running the following command: + +```bash +npm uninstall ckeditor5 ckeditor5-premium-features +``` + +Upgrade the CKEditor 5 Vue 3 integration to the latest version. You can find the latest version in the {@link getting-started/integrations-cdn/vuejs-v3 Vue 3 integration} documentation. + +Ensure that your testing suite uses real web browser environments for testing. If you are using `jsdom` or any other environment without a real DOM, you may need to adjust the testing suite configuration to use a real browser because CDN script injection might not be recognized properly in such environments. + +## Migration steps + +### Step 1: Remove CKEditor 5 imports + +If you have any CKEditor 5 imports in your Vue components, remove them. For example, remove lines like: + +```javascript +import { ClassicEditor, /* ... other imports */ } from 'ckeditor5'; +import { AIAdapter, /* ... other imports */ } from 'ckeditor5-premium-features'; +``` + +### Step 2: Update your Vue components to use CDN + +Replace the CKEditor 5 NPM package imports with the CDN script imports and use the `useCKEditorCloud` function to load the CKEditor 5 scripts. The `useCKEditorCloud` function is a part of the `@ckeditor/ckeditor5-vue` package and is used to load CKEditor 5 scripts from the CKEditor Cloud service. + +**Before:** + +```html + + + +``` + +**After:** + +```html + + + +``` + +### Step 3 (Optional): Migrate the CKEditor 5 Vue 3+ integration testing suite + +If you have any tests that use CKEditor 5 objects, you need to update them to use the `loadCKEditorCloud` function. Here is an example of migrating a test that uses the `ClassicEditor` object: + +**Before:** + +```javascript +import { ClassicEditor, /* ... other imports */ } from 'ckeditor5'; + +it( 'ClassicEditor test', () => { + // Your test that uses the CKEditor 5 object. +} ); +``` + +**After:** + +```javascript +// It may be counterintuitive that in tests you need to use `loadCKEditorCloud` instead of `useCKEditorCloud`. +// The reason for this is that `useCKEditorCloud` is composable and can only be used in Vue components, +// while tests are typically written as functions in testing suites. Therefore, in tests, you should use +// the `loadCKEditorCloud` function to load CKEditor 5 from the CKEditor Cloud and obtain the necessary +// CKEditor 5 objects. This allows you to properly test your CKEditor 5 integration without any issues. + +import { loadCKEditorCloud } from '@ckeditor/ckeditor5-vue'; + +let cloud; + +beforeEach( async () => { + cloud = await loadCKEditorCloud( { + version: '{@var ckeditor5-version}', + } ); +} ); + +it( 'ClassicEditor test', () => { + const { ClassicEditor, ... } = cloud.CKEditor; + + // Your test that uses the CKEditor 5 object. +} ); +``` + +#### Step 4 (Optional): Clean up the document head entries before each test + +The `useCKEditorCloud` composable under the hood injects the CKEditor 5 scripts and styles into your document head. If you use a testing suite that does not Clean up the document head entries before each test, you may need to do it manually. This is important because the `useCKEditorCloud` composable might reuse the same head entries for each test, which can lead to skipping the `loading` state and directly going to the `success` state. It may cause some tests that rely on the `loading` state to fail. + +However, there is one downside to this approach. Cleaning up the head entries before each test may slow down the test execution because the browser needs to download the CKEditor 5 script each time. In most cases, this should not be a problem, but if you notice that your tests are running slower, you may need to consider other solutions. + +Here is an example of how you can Clean up the document head entries before each test: + +```javascript +import { removeAllCkCdnResources } from '@ckeditor/ckeditor5-integrations-common/test-utils'; + +beforeEach( () => { + removeAllCkCdnResources(); +} ); +``` + +The code above will remove all CKEditor 5 CDN scripts, style sheets, and Window objects from the head section of your HTML file before each test, making sure that the `useCKEditorCloud` composable will inject the CKEditor 5 scripts and styles again. diff --git a/docs/updating/nim-migration/predefined-builds.md b/docs/updating/nim-migration/predefined-builds.md index 626229974fe..50c3d46fc8b 100644 --- a/docs/updating/nim-migration/predefined-builds.md +++ b/docs/updating/nim-migration/predefined-builds.md @@ -1722,7 +1722,6 @@ One notable difference between the old build and the new ESM build is that the f toolbar: [ 'aiCommands', 'aiAssistant', '|', - // Productivity pack. 'tableOfContents', 'formatPainter', 'insertTemplate', 'caseChange', '|', 'pagination', diff --git a/docs/updating/update-to-38.md b/docs/updating/update-to-38.md index ee129080916..d73c1d1aee9 100644 --- a/docs/updating/update-to-38.md +++ b/docs/updating/update-to-38.md @@ -45,7 +45,7 @@ Starting from version 38.0.0, all **open source installations** of CKEditor  If you have a **commercial license**, you can hide the logo by adding {@link module:core/editor/editorconfig~EditorConfig#licenseKey `config.licenseKey`} to your configuration. If you already use pagination, productivity pack, or asynchronous collaboration features, you do not need to take any action as you should already have `config.licenseKey` in place. The logo will not be visible in your editor. -We have prepared a detailed {@link getting-started/setup/managing-ckeditor-logo Managing the "Powered by CKEditor" logo} guide to help everyone through the transition and explain any concerns. +We have prepared a detailed {@link getting-started/licensing/managing-ckeditor-logo Managing the "Powered by CKEditor" logo} guide to help everyone through the transition and explain any concerns. ### Introduction of color pickers to font color and font background color features diff --git a/package.json b/package.json index ee84d59d195..d10360fead0 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,7 @@ "coveralls": "^3.1.0", "date-fns": "^2.30.0", "eslint": "^7.19.0", - "eslint-config-ckeditor5": "^7.0.0", + "eslint-config-ckeditor5": "^7.1.0", "estree-walker": "^3.0.3", "fs-extra": "^11.1.1", "glob": "^10.2.5", diff --git a/packages/ckeditor5-alignment/docs/_snippets/features/custom-text-alignment-options.js b/packages/ckeditor5-alignment/docs/_snippets/features/custom-text-alignment-options.js index c74ca218d76..c37942d410f 100644 --- a/packages/ckeditor5-alignment/docs/_snippets/features/custom-text-alignment-options.js +++ b/packages/ckeditor5-alignment/docs/_snippets/features/custom-text-alignment-options.js @@ -39,7 +39,8 @@ ClassicEditor alignment: { options: [ 'left', 'right' ] }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-alignment/docs/_snippets/features/custom-text-alignment-toolbar.js b/packages/ckeditor5-alignment/docs/_snippets/features/custom-text-alignment-toolbar.js index 8eb648d8662..5beb4e8e40a 100644 --- a/packages/ckeditor5-alignment/docs/_snippets/features/custom-text-alignment-toolbar.js +++ b/packages/ckeditor5-alignment/docs/_snippets/features/custom-text-alignment-toolbar.js @@ -36,7 +36,8 @@ ClassicEditor allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-alignment/docs/_snippets/features/text-alignment.js b/packages/ckeditor5-alignment/docs/_snippets/features/text-alignment.js index f0d0392c4c2..7790e2e2516 100644 --- a/packages/ckeditor5-alignment/docs/_snippets/features/text-alignment.js +++ b/packages/ckeditor5-alignment/docs/_snippets/features/text-alignment.js @@ -36,7 +36,8 @@ ClassicEditor allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-alignment/docs/features/text-alignment.md b/packages/ckeditor5-alignment/docs/features/text-alignment.md index 70034d4183a..29f6d64e08c 100644 --- a/packages/ckeditor5-alignment/docs/features/text-alignment.md +++ b/packages/ckeditor5-alignment/docs/features/text-alignment.md @@ -26,15 +26,19 @@ Click inside a paragraph or a header and use the toolbar dropdown {@icon @ckedit Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, Alignment } from 'ckeditor5'; ClassicEditor. create( document.querySelector( '#editor' ), { + licenseKey: '' // Or 'GPL'. plugins: [ Alignment, /* ... */ ], toolbar: [ 'alignment', /* ... */ ] + alignment: { + // Configuration. + } } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -55,6 +59,7 @@ For example, the following editor will support two alignment options: to the lef ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... alignment: { options: [ 'left', 'right' ] }, @@ -81,6 +86,7 @@ The following configuration will set `.my-align-left` and `.my-align-right` to l ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... alignment: { options: [ { name: 'left', className: 'my-align-left' }, @@ -102,6 +108,7 @@ You can choose to use the alignment dropdown (`'alignment'`) or configure the to ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... toolbar: [ 'heading', '|', 'alignment:left', 'alignment:right', 'alignment:center', 'alignment:justify' ] diff --git a/packages/ckeditor5-autoformat/docs/_snippets/features/autoformat.js b/packages/ckeditor5-autoformat/docs/_snippets/features/autoformat.js index 2f9f546376c..12a04b405b8 100644 --- a/packages/ckeditor5-autoformat/docs/_snippets/features/autoformat.js +++ b/packages/ckeditor5-autoformat/docs/_snippets/features/autoformat.js @@ -59,7 +59,8 @@ ClassicEditor allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editorBasic = editor; diff --git a/packages/ckeditor5-autoformat/docs/features/autoformat.md b/packages/ckeditor5-autoformat/docs/features/autoformat.md index 379f540d545..1fba6d24acd 100644 --- a/packages/ckeditor5-autoformat/docs/features/autoformat.md +++ b/packages/ckeditor5-autoformat/docs/features/autoformat.md @@ -52,13 +52,14 @@ The following {@link features/basic-styles basic styles} inline formatting optio Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, Autoformat } from 'ckeditor5'; ClassicEditor. create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Autoformat, /* ... */ ], toolbar: [ /* ... */ ] } ) diff --git a/packages/ckeditor5-autosave/docs/_snippets/features/autosave.js b/packages/ckeditor5-autosave/docs/_snippets/features/autosave.js index 4addcc2259f..354b569a6d1 100644 --- a/packages/ckeditor5-autosave/docs/_snippets/features/autosave.js +++ b/packages/ckeditor5-autosave/docs/_snippets/features/autosave.js @@ -42,7 +42,8 @@ ClassicEditor save( editor ) { return saveData( editor.getData() ); } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-autosave/docs/features/autosave.md b/packages/ckeditor5-autosave/docs/features/autosave.md index e46740e5c2e..435c79bcc6b 100644 --- a/packages/ckeditor5-autosave/docs/features/autosave.md +++ b/packages/ckeditor5-autosave/docs/features/autosave.md @@ -37,7 +37,7 @@ How to understand this demo: Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list. +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list. Assuming that you have implemented some form of the `saveData()` function that sends the data to your server and returns a promise which is resolved once the data is successfully saved, configuring the {@link module:autosave/autosave~Autosave} feature is simple: @@ -46,19 +46,12 @@ import { ClassicEditor, Autosave } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ - Autosave, - - // ... other plugins. - ], + licenseKey: '', // Or 'GPL'. + plugins: [ Autosave, /* ... */ ], autosave: { - save( editor ) { - return saveData( editor.getData() ); - } - }, - - // ... other configuration options. + // Configuration. + } } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -82,12 +75,11 @@ One second is the default waiting time before the next save action if nothing ha ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... autosave: { waitingTime: 5000, // in ms save( editor ) {} }, - - // ... other configuration options. } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -100,12 +92,7 @@ The demo example at the beginning of this guide shows a simple integration of th ```js ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ - Autosave, - - // ... other plugins. - ], - + // ... Other configuration options ... autosave: { save( editor ) { return saveData( editor.getData() ); diff --git a/packages/ckeditor5-basic-styles/docs/_snippets/features/basic-styles.js b/packages/ckeditor5-basic-styles/docs/_snippets/features/basic-styles.js index 84179fa0503..347a661910c 100644 --- a/packages/ckeditor5-basic-styles/docs/_snippets/features/basic-styles.js +++ b/packages/ckeditor5-basic-styles/docs/_snippets/features/basic-styles.js @@ -34,7 +34,8 @@ ClassicEditor allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-basic-styles/docs/features/basic-styles.md b/packages/ckeditor5-basic-styles/docs/features/basic-styles.md index 8c7573ce673..f6f9fdfcd5e 100644 --- a/packages/ckeditor5-basic-styles/docs/features/basic-styles.md +++ b/packages/ckeditor5-basic-styles/docs/features/basic-styles.md @@ -72,13 +72,14 @@ CKEditor 5 allows for typing both at the inner and outer boundaries of code Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the plugins which you need to your plugin list. Then, simply configure the toolbar items to make the features available in the user interface. +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the plugins which you need to your plugin list. Then, simply configure the toolbar items to make the features available in the user interface. ```js import { ClassicEditor, Bold, Code, Italic, Strikethrough, Subscript, Superscript, Underline } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Bold, Code, Italic, Strikethrough, Subscript, Superscript, Underline ], toolbar: { items: [ 'bold', 'italic', 'underline', 'strikethrough', 'code', 'subscript', 'superscript' ] diff --git a/packages/ckeditor5-block-quote/docs/_snippets/features/block-quote-source.js b/packages/ckeditor5-block-quote/docs/_snippets/features/block-quote-source.js index eb60d3bbeaf..0cf4ad97c44 100644 --- a/packages/ckeditor5-block-quote/docs/_snippets/features/block-quote-source.js +++ b/packages/ckeditor5-block-quote/docs/_snippets/features/block-quote-source.js @@ -45,7 +45,8 @@ ClassicEditor.defaultConfig = { }, image: { toolbar: [ 'toggleImageCaption', 'imageTextAlternative', 'ckboxImageEdit' ] - } + }, + licenseKey: 'GPL' }; window.ClassicEditor = ClassicEditor; diff --git a/packages/ckeditor5-block-quote/docs/features/block-quote.md b/packages/ckeditor5-block-quote/docs/features/block-quote.md index 3af0af0ca15..0e0167ebcb6 100644 --- a/packages/ckeditor5-block-quote/docs/features/block-quote.md +++ b/packages/ckeditor5-block-quote/docs/features/block-quote.md @@ -39,13 +39,14 @@ Support for nested block quotes is provided as backward compatibility for loadin Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, BlockQuote } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ BlockQuote, /* ... */ ], toolbar: [ 'blockQuote', /* ... */ ] } ) diff --git a/packages/ckeditor5-ckbox/docs/_snippets/features/ckbox.js b/packages/ckeditor5-ckbox/docs/_snippets/features/ckbox.js index 57da1b01389..d27c9be1f64 100644 --- a/packages/ckeditor5-ckbox/docs/_snippets/features/ckbox.js +++ b/packages/ckeditor5-ckbox/docs/_snippets/features/ckbox.js @@ -61,7 +61,8 @@ ClassicEditor tokenUrl: TOKEN_URL, forceDemoLabel: true, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-ckbox/docs/features/ckbox.md b/packages/ckeditor5-ckbox/docs/features/ckbox.md index 72974b56591..6cd96985d55 100644 --- a/packages/ckeditor5-ckbox/docs/features/ckbox.md +++ b/packages/ckeditor5-ckbox/docs/features/ckbox.md @@ -15,12 +15,7 @@ badges: [ premium ] CKBox is a dedicated asset manager supporting file and image upload. The CKBox feature lets you easily upload various files and insert images and links to other files into your content. It also offers image conversion and optimization capabilities and provides a {@link features/images-responsive responsive images mechanism} for CKEditor 5. - This is a premium feature and you need a subscription to use it. You can [purchase it here](https://ckeditor.com/pricing/) for your open-source CKEditor implementation. [Contact us](https://ckeditor.com/contact/?sales=true#contact-form) if: - * CKEditor commercial license is needed for your application. - * You need the **on-premises (self-hosted)** version of the service. - * You have other licensing questions. - - You can also sign up for the [CKEditor Premium Features 30-day free trial](https://orders.ckeditor.com/trial/premium-features) to test the feature. + Unlock this feature with a CKEditor Paid Plan. [Sign up for a free trial](https://portal.ckeditor.com/checkout?plan=free), or [select the Plan](https://ckeditor.com/pricing/) that provides access to all the premium features you need. ## How CKBox enhances CKEditor 5 @@ -90,11 +85,11 @@ import { ClassicEditor, Image, ImageUpload, PictureEditing, CKBox, CKBoxImageEdi ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Image, PictureEditing, ImageUpload, CloudServices, CKBox, CKBoxImageEdit, /* ... */ ], toolbar: [ 'ckbox', 'ckboxImageEdit', /* ... */ ], // Depending on your preference. ckbox: { - // Feature configuration including license key. - // ... + // Configuration. } } ) .then( /* ... */ ) @@ -114,6 +109,7 @@ The snippet below shows an example image contextual toolbar configuration. ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... image: { toolbar: [ 'toggleImageCaption', 'imageTextAlternative', 'ckboxImageEdit' ] } @@ -131,7 +127,7 @@ The feature can be configured via the {@link module:ckbox/ckboxconfig~CKBoxConfi This is a premium feature. [Contact us](https://ckeditor.com/contact/?sales=true#contact-form) to receive an offer tailored to your needs. - You can also sign up for the [CKEditor Premium Features 30-day free trial](https://orders.ckeditor.com/trial/premium-features) to test the feature. + You can also sign up for the [CKEditor Premium Features 14-day free trial](https://orders.ckeditor.com/trial/premium-features) to test the feature. If you already have a valid license, please log into your [user dashboard](https://dashboard.ckeditor.com/) to access the feature settings. @@ -143,12 +139,9 @@ After you purchase a license, log into the CKEditor Ecosystem customer dashboard By default, the CKBox feature maps the uploaded image type to the category configured on the cloud service. You can override this behavior and provide your own mappings via the {@link module:ckbox/ckboxconfig~CKBoxConfig#defaultUploadCategories `config.ckbox.defaultUploadCategories`} configuration option. It is an object, where the keys define categories and their values are the types of images that will be uploaded to these categories. The categories might be referenced either by their name or by their ID. Referencing by ID is future-proof because it will not require configuration changes when a category name changes. ```js -import { ClassicEditor, CKBox } from 'ckeditor5'; - ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ CKBox, /* ... */ ], - toolbar: [ 'ckbox', /* ... */ ], + // ... Other configuration options ... ckbox: { defaultUploadCategories: { Bitmaps: [ 'bmp' ], @@ -178,12 +171,9 @@ The [CKBox workspaces](https://ckeditor.com/docs/ckbox/latest/features/file-mana If the user is assigned to more than one workspace, by default all the files uploaded directly from CKEditor are located in the first workspace in the list of workspaces allowed in the user's JWT token. This corresponds to uploads through drag and drop into the editor area, pasting images from the clipboard, or images uploaded using the Image {@icon @ckeditor/ckeditor5-core/theme/icons/image-upload.svg Image} feature. If you would like to define a specific workspace for files uploaded this way, you can define its ID in the `defaultUploadWorkspaceId` option. After that, all the files uploaded directly from CKEditor will be placed in the specified workspace. ```js -import { ClassicEditor, CKBox } from 'ckeditor5'; - ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ CKBox, /* ... */ ], - toolbar: [ 'ckbox', /* ... */ ], + // ... Other configuration options ... ckbox: { tokenUrl: 'https://your.token.url', // Sample workspace referenced by its ID. @@ -201,12 +191,9 @@ You can obtain the list of available workspaces using the [Workspaces REST API]( After choosing an asset from the CKBox dialog, it is inserted into the editor content with a unique `data-ckbox-resource-id` attribute. If you want to disable it and do not want to add this attribute, set the {@link module:ckbox/ckboxconfig~CKBoxConfig#ignoreDataId `config.ckbox.ignoreDataId`} option to `true`: ```js -import { ClassicEditor, CKBox } from 'ckeditor5'; - ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ CKBox, /* ... */ ], - toolbar: [ 'ckbox', /* ... */ ], + // ... Other configuration options ... ckbox: { ignoreDataId: true } @@ -220,12 +207,9 @@ ClassicEditor By default, the CKBox dialog takes the current language from the editor. If you want to use a different language, you can set the language code in the {@link module:ckbox/ckboxconfig~CKBoxConfig#language `config.ckbox.language`} option: ```js -import { ClassicEditor, CKBox } from 'ckeditor5'; - ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ CKBox, /* ... */ ], - toolbar: [ 'ckbox', /* ... */ ], + // ... Other configuration options ... ckbox: { language: 'es' } @@ -246,12 +230,9 @@ Also, make sure to include the translation file after loading the CKBox library: The CKBox feature requires the token endpoint URL configured in the {@link module:ckbox/ckboxconfig~CKBoxConfig#tokenUrl `config.ckbox.tokenUrl`} key. If not explicitly provided, the token URL from {@link module:cloud-services/cloudservicesconfig~CloudServicesConfig#tokenUrl `config.cloudServices.tokenUrl`} is used instead. If both are provided, the token URL defined in `config.ckbox.tokenUrl` takes precedence over the `config.cloudServices.tokenUrl`. ```js -import { ClassicEditor, CKBox } from 'ckeditor5'; - ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ CKBox, /* ... */ ], - toolbar: [ 'ckbox', /* ... */ ], + // ... Other configuration options ... ckbox: { tokenUrl: 'https://example.com/cs-token-endpoint' } @@ -265,12 +246,9 @@ ClassicEditor If you host the cloud service in your environment, you should configure the base URL of the API service via the {@link module:ckbox/ckboxconfig~CKBoxConfig#serviceOrigin `config.ckbox.serviceOrigin`} option: ```js -import { ClassicEditor, CKBox } from 'ckeditor5'; - ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ CKBox, /* ... */ ], - toolbar: [ 'ckbox', /* ... */ ], + // ... Other configuration options ... ckbox: { serviceOrigin: 'https://example.com/' } @@ -284,12 +262,9 @@ ClassicEditor If you want to allow CKBox to edit external images, not hosted by the file manager (for example, pasted via URL) you need to whitelist the URLs of the images. You can do this using the {@link module:ckbox/ckboxconfig~CKBoxConfig#allowExternalImagesEditing `config.ckbox.allowExternalImagesEditing`} option: ```js -import { CKBox } from 'ckeditor5'; - ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ CKBox, /* ... */ ], - toolbar: [ 'ckbox', /* ... */ ], + // ... Other configuration options ... ckbox: { allowExternalImagesEditing: [ 'origin', /^cksource.com/ ] } diff --git a/packages/ckeditor5-ckfinder/docs/_snippets/features/ckfinder-options.js b/packages/ckeditor5-ckfinder/docs/_snippets/features/ckfinder-options.js index 64adfb20b84..9dd341d3e8f 100644 --- a/packages/ckeditor5-ckfinder/docs/_snippets/features/ckfinder-options.js +++ b/packages/ckeditor5-ckfinder/docs/_snippets/features/ckfinder-options.js @@ -29,7 +29,8 @@ ClassicEditor width: 800, resourceType: 'Images' } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-ckfinder/docs/_snippets/features/ckfinder-upload-only.js b/packages/ckeditor5-ckfinder/docs/_snippets/features/ckfinder-upload-only.js index ba348ae843c..fe6ff0f1240 100644 --- a/packages/ckeditor5-ckfinder/docs/_snippets/features/ckfinder-upload-only.js +++ b/packages/ckeditor5-ckfinder/docs/_snippets/features/ckfinder-upload-only.js @@ -56,7 +56,8 @@ ClassicEditor ckfinder: { // eslint-disable-next-line max-len uploadUrl: 'https://ckeditor.com/apps/ckfinder/3.5.0/core/connector/php/connector.php?command=QuickUpload&type=Files&responseType=json' - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-ckfinder/docs/_snippets/features/ckfinder.js b/packages/ckeditor5-ckfinder/docs/_snippets/features/ckfinder.js index 836271cdeae..3984c74c41d 100644 --- a/packages/ckeditor5-ckfinder/docs/_snippets/features/ckfinder.js +++ b/packages/ckeditor5-ckfinder/docs/_snippets/features/ckfinder.js @@ -60,7 +60,8 @@ ClassicEditor height: 600, width: 800 } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-ckfinder/docs/features/ckfinder.md b/packages/ckeditor5-ckfinder/docs/features/ckfinder.md index 5d4d044e2e6..12ee31cbf1a 100644 --- a/packages/ckeditor5-ckfinder/docs/features/ckfinder.md +++ b/packages/ckeditor5-ckfinder/docs/features/ckfinder.md @@ -13,10 +13,8 @@ badges: [ premium ] The CKFinder feature lets you insert images and links to files into your content. CKFinder is a powerful file manager with various image editing and image upload options. - - This is a premium feature and you need a license for it on top of your CKEditor 5 commercial license. [Contact us](https://ckeditor.com/contact/?sales=true#contact-form) to receive an offer tailored to your needs. - - You can also sign up for the [CKEditor Premium Features 30-day free trial](https://orders.ckeditor.com/trial/premium-features) to test the feature. + + Unlock this feature with a CKEditor Paid Plan. [Sign up for a free trial](https://portal.ckeditor.com/checkout?plan=free), or [select the Plan](https://ckeditor.com/pricing/) that provides access to all the premium features you need. ## Demos @@ -70,18 +68,18 @@ You can use this feature in the rich-text editor in two different ways: Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, CKFinder, CKFinderUploadAdapter } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ CKFinder, CKFinderUploadAdapter, /* ... */ ], toolbar: [ 'ckfinder', 'uploadImage', /* ... */ ], // Depending on your preference. ckfinder: { - // Feature configuration. - // ... + // Configuration. } } ) .then( /* ... */ ) @@ -99,15 +97,9 @@ This feature can upload images automatically to the server (for example, when th Assuming that you [installed the CKFinder PHP server-side connector](https://ckeditor.com/docs/ckfinder/ckfinder3-php/quickstart.html#quickstart_installation_folders) (and it is available under `https://example.com/ckfinder/`), use the following [quick upload](https://ckeditor.com/docs/ckfinder/ckfinder3-php/commands.html#command_quick_upload) command URL to enable the image upload: ```js -import { ClassicEditor, CKFinder, CKFinderUploadAdapter } from 'ckeditor5'; - ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ CKFinder, CKFinderUploadAdapter, /* ... */ ], - - // Enable the insert image button in the toolbar. - toolbar: [ 'uploadImage', /* ... */ ], - + // ... Other configuration options ... ckfinder: { // Upload the images to the server using the CKFinder QuickUpload command. uploadUrl: 'https://example.com/ckfinder/core/connector/php/connector.php?command=QuickUpload&type=Images&responseType=json' @@ -139,15 +131,9 @@ Then: * You can define [`options.language`](https://ckeditor.com/docs/ckfinder/ckfinder3/#!/api/CKFinder.Config-cfg-language) to set the UI language of CKFinder. By default, it will be set to the UI language of the editor. ```js -import { ClassicEditor, CKFinder, CKFinderUploadAdapter } from 'ckeditor5'; - ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ CKFinder, CKFinderUploadAdapter, /* ... */ ], - - // Enable the CKFinder button in the toolbar. - toolbar: [ 'ckfinder', /* ... */ ] - + // ... Other configuration options ... ckfinder: { // Upload the images to the server using the CKFinder QuickUpload command. uploadUrl: 'https://example.com/ckfinder/core/connector/php/connector.php?command=QuickUpload&type=Images&responseType=json', @@ -171,12 +157,9 @@ You can change the way CKFinder opens using the {@link module:ckfinder/ckfinderc By default, the file manager opens as a modal. To open it in a new pop-up window, set the configuration value to `popup`: ```js -import { ClassicEditor, CKFinder, CKFinderUploadAdapter } from 'ckeditor5'; - ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ CKFinder, CKFinderUploadAdapter, /* ... */ ], - toolbar: [ 'ckfinder', /* ... */ ] + // ... Other configuration options ... ckfinder: { // Open the file manager in the pop-up window. openerMethod: 'popup' diff --git a/packages/ckeditor5-clipboard/docs/_snippets/features/build-drag-drop-source.js b/packages/ckeditor5-clipboard/docs/_snippets/features/build-drag-drop-source.js index 3bf8a297f8d..bd569d2c5eb 100644 --- a/packages/ckeditor5-clipboard/docs/_snippets/features/build-drag-drop-source.js +++ b/packages/ckeditor5-clipboard/docs/_snippets/features/build-drag-drop-source.js @@ -143,7 +143,8 @@ const defaultConfig = { options: [ 10, 12, 14, 'default', 18, 20, 22 ], supportAllValues: true }, - language: 'en' + language: 'en', + licenseKey: 'GPL' }; class ClassicEditor extends ClassicEditorBase {} diff --git a/packages/ckeditor5-clipboard/docs/_snippets/features/paste-plain-text.js b/packages/ckeditor5-clipboard/docs/_snippets/features/paste-plain-text.js index 54e67c3eab7..94b59d3ead1 100644 --- a/packages/ckeditor5-clipboard/docs/_snippets/features/paste-plain-text.js +++ b/packages/ckeditor5-clipboard/docs/_snippets/features/paste-plain-text.js @@ -43,7 +43,8 @@ ClassicEditor supportAllValues: true }, placeholder: 'Paste the content here to test the feature.', - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-clipboard/docs/features/drag-drop.md b/packages/ckeditor5-clipboard/docs/features/drag-drop.md index 881d5cc4e0f..941c7577a34 100644 --- a/packages/ckeditor5-clipboard/docs/features/drag-drop.md +++ b/packages/ckeditor5-clipboard/docs/features/drag-drop.md @@ -59,13 +59,14 @@ In the balloon block editor, you can also drag content blocks using the drag han Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, Clipboard } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Clipboard, Bold, /* ... */ ], }) .then( /* ... */ ) diff --git a/packages/ckeditor5-clipboard/docs/features/paste-plain-text.md b/packages/ckeditor5-clipboard/docs/features/paste-plain-text.md index f6b9fa324a1..c22050c812a 100644 --- a/packages/ckeditor5-clipboard/docs/features/paste-plain-text.md +++ b/packages/ckeditor5-clipboard/docs/features/paste-plain-text.md @@ -37,13 +37,14 @@ Pasting plain text with a double line break will turn the break into a paragraph Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { Bold, ClassicEditor, Clipboard } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Clipboard, Bold, /* ... */ ] } ) .then( /* ... */ ) diff --git a/packages/ckeditor5-code-block/docs/_snippets/features/code-block-custom-languages.js b/packages/ckeditor5-code-block/docs/_snippets/features/code-block-custom-languages.js index ded4e84d46e..c3cf93e86d3 100644 --- a/packages/ckeditor5-code-block/docs/_snippets/features/code-block-custom-languages.js +++ b/packages/ckeditor5-code-block/docs/_snippets/features/code-block-custom-languages.js @@ -40,7 +40,8 @@ ClassicEditor { language: 'css', label: 'CSS' }, { language: 'html', label: 'HTML' } ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-code-block/docs/_snippets/features/code-block.js b/packages/ckeditor5-code-block/docs/_snippets/features/code-block.js index c1ee3857c9f..20648dddd04 100644 --- a/packages/ckeditor5-code-block/docs/_snippets/features/code-block.js +++ b/packages/ckeditor5-code-block/docs/_snippets/features/code-block.js @@ -34,7 +34,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-code-block/docs/features/code-blocks.md b/packages/ckeditor5-code-block/docs/features/code-blocks.md index 8a12e123fcb..c338ec07c2c 100644 --- a/packages/ckeditor5-code-block/docs/features/code-blocks.md +++ b/packages/ckeditor5-code-block/docs/features/code-blocks.md @@ -29,15 +29,19 @@ Each code block has a [specific programming language assigned](#configuring-code Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, CodeBlock } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ CodeBlock, /* ... */ ], toolbar: [ 'codeBlock', /* ... */ ] + codeBlock: { + // Configuration. + } } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -56,6 +60,7 @@ It is possible to configure which languages are available to the users. You can ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... codeBlock: { languages: [ { language: 'css', label: 'CSS' }, @@ -74,6 +79,7 @@ By default, the CSS class of the `` element in the data and editing is gen ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... codeBlock: { languages: [ // Do not render the CSS class for the plain text code blocks. diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index ff82a2cd9c5..c385de96fae 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -11,9 +11,16 @@ import { Config, CKEditorError, ObservableMixin, + logError, + parseBase64EncodedObject, + releaseDate, + toArray, + uid, + crc32, type Locale, type LocaleTranslate, - type ObservableChangeEvent + type ObservableChangeEvent, + type CRCData } from '@ckeditor/ckeditor5-utils'; import { @@ -36,6 +43,11 @@ import Accessibility from '../accessibility.js'; import type { LoadedPlugins, PluginConstructor } from '../plugin.js'; import type { EditorConfig } from './editorconfig.js'; +declare global { + // eslint-disable-next-line no-var + var CKEDITOR_GLOBAL_LICENSE_KEY: string | undefined; +} + /** * The class representing a basic, generic editor. * @@ -312,6 +324,8 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { this.config.define( 'plugins', availablePlugins ); this.config.define( this._context._getEditorConfig() ); + checkLicenseKeyIsDefined( this.config ); + this.plugins = new PluginCollection( this, availablePlugins, this._context.plugins ); this.locale = this._context.locale; @@ -346,6 +360,193 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { this.keystrokes.listenTo( this.editing.view.document ); this.accessibility = new Accessibility( this ); + + verifyLicenseKey( this ); + + // Checks if the license key is defined and throws an error if it is not. + function checkLicenseKeyIsDefined( config: Config ) { + let licenseKey = config.get( 'licenseKey' ); + + if ( !licenseKey && window.CKEDITOR_GLOBAL_LICENSE_KEY ) { + licenseKey = window.CKEDITOR_GLOBAL_LICENSE_KEY; + config.set( 'licenseKey', licenseKey ); + } + + if ( !licenseKey ) { + /** + * The licenseKey is missing. Add your license or 'GPL' string to the editor config. + * + * @error editor-license-key-missing + */ + throw new CKEditorError( 'editor-license-key-missing' ); + } + } + + function verifyLicenseKey( editor: Editor ) { + const licenseKey = editor.config.get( 'licenseKey' )!; + const distributionChannel = ( window as any )[ Symbol.for( 'cke distribution' ) ] || 'sh'; + + if ( licenseKey == 'GPL' ) { + if ( distributionChannel == 'cloud' ) { + blockEditor( 'distributionChannel' ); + } + + return; + } + + const encodedPayload = getPayload( licenseKey ); + + if ( !encodedPayload ) { + blockEditor( 'invalid' ); + + return; + } + + const licensePayload = parseBase64EncodedObject( encodedPayload ); + + if ( !licensePayload ) { + blockEditor( 'invalid' ); + + return; + } + + if ( !hasAllRequiredFields( licensePayload ) ) { + blockEditor( 'invalid' ); + + return; + } + + if ( licensePayload.distributionChannel && !toArray( licensePayload.distributionChannel ).includes( distributionChannel ) ) { + blockEditor( 'distributionChannel' ); + + return; + } + + if ( crc32( getCrcInputData( licensePayload ) ) != licensePayload.vc.toLowerCase() ) { + blockEditor( 'invalid' ); + + return; + } + + const expirationDate = new Date( licensePayload.exp * 1000 ); + + if ( expirationDate < releaseDate ) { + blockEditor( 'expired' ); + + return; + } + + const licensedHosts: Array | undefined = licensePayload.licensedHosts; + + if ( licensedHosts && licensedHosts.length > 0 && !checkLicensedHosts( licensedHosts ) ) { + blockEditor( 'domainLimit' ); + + return; + } + + if ( [ 'evaluation', 'trial' ].includes( licensePayload.licenseType ) && licensePayload.exp * 1000 < Date.now() ) { + blockEditor( 'expired' ); + + return; + } + + if ( [ 'evaluation', 'trial', 'development' ].includes( licensePayload.licenseType ) ) { + const licenseType: 'evaluation' | 'trial' | 'development' = licensePayload.licenseType; + + console.info( + `You are using the ${ licenseType } version of CKEditor 5 with limited usage. ` + + 'Make sure you will not use it in the production environment.' + ); + + const timerId = setTimeout( () => { + blockEditor( `${ licenseType }Limit` ); + }, 600000 ); + + editor.on( 'destroy', () => { + clearTimeout( timerId ); + } ); + } + + if ( licensePayload.usageEndpoint ) { + editor.once( 'ready', () => { + const telemetry = editor._getTelemetryData(); + + const request = { + requestId: uid(), + requestTime: Math.round( Date.now() / 1000 ), + license: licenseKey, + telemetry + }; + + editor._sendUsageRequest( licensePayload.usageEndpoint, request ).then( response => { + const { status, message } = response; + + if ( message ) { + console.warn( message ); + } + + if ( status != 'ok' ) { + blockEditor( 'usageLimit' ); + } + }, () => { + /** + * Your license key cannot be validated because of a network issue. + * Please make sure that your setup does not block the request. + * + * @error license-key-validation-endpoint-not-reachable + * @param {String} url The URL that was attempted to reach. + */ + logError( 'license-key-validation-endpoint-not-reachable', { url: licensePayload.usageEndpoint } ); + } ); + }, { priority: 'high' } ); + } + + function getPayload( licenseKey: string ): string | null { + const parts = licenseKey.split( '.' ); + + if ( parts.length != 3 ) { + return null; + } + + return parts[ 1 ]; + } + + function blockEditor( reason: LicenseErrorReason ) { + editor.enableReadOnlyMode( Symbol( 'invalidLicense' ) ); + editor._showLicenseError( reason ); + } + + function hasAllRequiredFields( licensePayload: Record ) { + const requiredFields = [ 'exp', 'jti', 'vc' ]; + + return requiredFields.every( field => field in licensePayload ); + } + + function getCrcInputData( licensePayload: Record ): CRCData { + const keysToCheck = Object.getOwnPropertyNames( licensePayload ).sort(); + + const filteredValues = keysToCheck + .filter( key => key != 'vc' && licensePayload[ key ] != null ) + .map( key => licensePayload[ key ] ); + + return filteredValues as CRCData; + } + + function checkLicensedHosts( licensedHosts: Array ): boolean { + const { hostname } = new URL( window.location.href ); + + if ( licensedHosts.includes( hostname ) ) { + return true; + } + + const segments = hostname.split( '.' ); + + return licensedHosts + .filter( host => host.includes( '*' ) ) + .map( host => host.split( '.' ) ) + .some( octets => segments.every( ( segment, index ) => octets[ index ] === segment || octets[ index ] === '*' ) ); + } + } } /** @@ -675,8 +876,145 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { * Exposed as static editor field for easier access in editor builds. */ public static ContextWatchdog = ContextWatchdog; + + private _getTelemetryData() { + return { + editorVersion: globalThis.CKEDITOR_VERSION + }; + } + + private _showLicenseError( reason: LicenseErrorReason, pluginName?: string ) { + setTimeout( () => { + if ( reason == 'invalid' ) { + /** + * Invalid license key. Please contact our customer support at https://ckeditor.com/contact/. + * + * @error invalid-license-key + */ + throw new CKEditorError( 'invalid-license-key', this ); + } + + if ( reason == 'expired' ) { + /** + * Your license key has expired. Please renew your license at https://ckeditor.com/TODO/. + * + * @error license-key-expired + */ + throw new CKEditorError( 'license-key-expired', this ); + } + + if ( reason == 'domainLimit' ) { + /** + * The hostname is not allowed by your license. Please update your license configuration at https://ckeditor.com/TODO/. + * + * @error license-key-domain-limit + */ + throw new CKEditorError( 'license-key-domain-limit', this ); + } + + if ( reason == 'featureNotAllowed' ) { + /** + * The plugin is not allowed by your license. + * + * Please check your license or contact support at https://ckeditor.com/contact/ for more information. + * + * @error license-key-feature-not-allowed + * @param {String} pluginName + */ + throw new CKEditorError( 'license-key-feature-not-allowed', this, { pluginName } ); + } + + if ( reason == 'evaluationLimit' ) { + /** + * You have exhausted the evaluation usage limit. Restart the editor. + * + * Please contact our customer support to get full access at https://ckeditor.com/contact/. + * + * @error license-key-evaluation-limit + */ + throw new CKEditorError( 'license-key-evaluation-limit', this ); + } + + if ( reason == 'trialLimit' ) { + /** + * You have exhausted the trial usage limit. Restart the editor. + * + * Please contact our customer support to get full access at https://ckeditor.com/contact/. + * + * @error license-key-trial-limit + */ + throw new CKEditorError( 'license-key-trial-limit', this ); + } + + if ( reason == 'developmentLimit' ) { + /** + * You have reached the development usage limit. Restart the editor. + * + * Please contact our customer support to get full access at https://ckeditor.com/contact/. + * + * @error license-key-development-limit + */ + throw new CKEditorError( 'license-key-development-limit', this ); + } + + if ( reason == 'usageLimit' ) { + /** + * The editor usage limit has been reached. + * + * Visit Contact support to extend the limit at https://ckeditor.com/contact/. + * + * @error license-key-usage-limit + */ + throw new CKEditorError( 'license-key-usage-limit', this ); + } + + if ( reason == 'distributionChannel' ) { + /** + * The usage is not valid for this distribution channel. + * + * Please check your installation or contact support at https://ckeditor.com/contact/ for more information. + * + * @error license-key-distribution-channel + */ + throw new CKEditorError( 'license-key-distribution-channel', this ); + } + + /* istanbul ignore next -- @preserve */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const unreachable: never = reason; + }, 0 ); + + this._showLicenseError = () => {}; + } + + private async _sendUsageRequest( endpoint: string, request: unknown ) { + const headers = new Headers( { 'Content-Type': 'application/json' } ); + const response = await fetch( new URL( endpoint ), { + method: 'POST', + headers, + body: JSON.stringify( request ) + } ); + + if ( !response.ok ) { + // TODO: refine message. + throw new Error( `HTTP Response: ${ response.status }` ); + } + + return response.json(); + } } +type LicenseErrorReason = + 'invalid' | + 'expired' | + 'domainLimit' | + 'featureNotAllowed' | + 'evaluationLimit' | + 'trialLimit' | + 'developmentLimit' | + 'usageLimit' | + 'distributionChannel'; + /** * Fired when the {@link module:engine/controller/datacontroller~DataController#event:ready data} and all additional * editor components are ready. diff --git a/packages/ckeditor5-core/src/editor/editorconfig.ts b/packages/ckeditor5-core/src/editor/editorconfig.ts index c923d5ab6d5..e0b8eb60a2d 100644 --- a/packages/ckeditor5-core/src/editor/editorconfig.ts +++ b/packages/ckeditor5-core/src/editor/editorconfig.ts @@ -935,7 +935,7 @@ export interface PoweredByConfig { * * @default 'border' */ - position: 'inside' | 'border'; + position?: 'inside' | 'border'; /** * Allows choosing the side of the editing area where the logo will be displayed. @@ -945,7 +945,7 @@ export interface PoweredByConfig { * * @default 'right' */ - side: 'left' | 'right'; + side?: 'left' | 'right'; /** * Allows changing the label displayed next to the CKEditor logo. @@ -954,7 +954,7 @@ export interface PoweredByConfig { * * @default 'Powered by' */ - label: string | null; + label?: string | null; /** * The vertical distance the logo can be moved away from its default position. @@ -963,14 +963,14 @@ export interface PoweredByConfig { * * @default 5 */ - verticalOffset: number; + verticalOffset?: number; /** * The horizontal distance between the side of the editing root and the nearest side of the logo. * * @default 5 */ - horizontalOffset: number; + horizontalOffset?: number; /** * Allows to show the logo even if the valid commercial license is configured using diff --git a/packages/ckeditor5-core/src/plugincollection.ts b/packages/ckeditor5-core/src/plugincollection.ts index e860d9e8773..bc692b00658 100644 --- a/packages/ckeditor5-core/src/plugincollection.ts +++ b/packages/ckeditor5-core/src/plugincollection.ts @@ -371,7 +371,7 @@ export default class PluginCollection * that you tried loading plugins by name. However, unlike CKEditor 4, CKEditor 5 does not implement a "plugin loader". * This means that CKEditor 5 does not know where to load the plugin modules from. Therefore, you need to * provide each plugin through a reference (as a constructor function). Check out the examples in the - * {@glink getting-started/installation/quick-start Quick start} guide. + * {@glink getting-started/installation/cloud/quick-start Quick start} guide. * * @error plugincollection-plugin-not-found * @param plugin The name of the plugin which could not be loaded. diff --git a/packages/ckeditor5-core/tests/_utils-tests/generatelicensekey.js b/packages/ckeditor5-core/tests/_utils-tests/generatelicensekey.js new file mode 100644 index 00000000000..04130933454 --- /dev/null +++ b/packages/ckeditor5-core/tests/_utils-tests/generatelicensekey.js @@ -0,0 +1,70 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals atob */ + +import generateLicenseKey from '../_utils/generatelicensekey.js'; + +describe( 'generateLicenseKey util', () => { + describe( 'generateLicenseKey()', () => { + it( 'should generate a license key with custom properties', () => { + const { licenseKey } = generateLicenseKey( { + licensedHosts: [ 'example.com' ], + licenseType: 'trial', + usageEndpoint: 'https://example.com/usage', + distributionChannel: 'cdn', + whiteLabel: true + } ); + + const decodedPayload = JSON.parse( atob( licenseKey.split( '.' )[ 1 ] ) ); + + expect( decodedPayload.licensedHosts ).to.deep.equal( [ 'example.com' ] ); + expect( decodedPayload.licenseType ).to.equal( 'trial' ); + expect( decodedPayload.usageEndpoint ).to.equal( 'https://example.com/usage' ); + expect( decodedPayload.distributionChannel ).to.equal( 'cdn' ); + expect( decodedPayload.whiteLabel ).to.be.true; + } ); + + it( 'should generate a license key without header and tail', () => { + const { licenseKey } = generateLicenseKey( { + skipHeader: true, + skipTail: true + } ); + + expect( licenseKey.startsWith( 'foo.' ) ).to.be.false; + expect( licenseKey.endsWith( '.bar' ) ).to.be.false; + } ); + + it( 'should generate a license key with custom VC', () => { + const { licenseKey } = generateLicenseKey( { + customVc: 'abc123' + } ); + + const decodedPayload = JSON.parse( atob( licenseKey.split( '.' )[ 1 ] ) ); + + expect( decodedPayload.vc ).to.equal( 'abc123' ); + } ); + + it( 'should generate a license key with custom expiration date', () => { + const { licenseKey } = generateLicenseKey( { + isExpired: true + } ); + + const decodedPayload = JSON.parse( atob( licenseKey.split( '.' )[ 1 ] ) ); + + expect( decodedPayload.exp ).to.be.below( Date.now() / 1000 ); + } ); + + it( 'should generate a license key with custom jti', () => { + const { licenseKey } = generateLicenseKey( { + jtiExist: false + } ); + + const decodedPayload = JSON.parse( atob( licenseKey.split( '.' )[ 1 ] ) ); + + expect( decodedPayload.jti ).to.be.undefined; + } ); + } ); +} ); diff --git a/packages/ckeditor5-core/tests/_utils/generatelicensekey.js b/packages/ckeditor5-core/tests/_utils/generatelicensekey.js new file mode 100644 index 00000000000..820898cc24f --- /dev/null +++ b/packages/ckeditor5-core/tests/_utils/generatelicensekey.js @@ -0,0 +1,80 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals btoa */ + +import { releaseDate, crc32 } from '@ckeditor/ckeditor5-utils'; + +/** + * Generates a license key for testing purposes. + */ +export default function generateKey( options = {} ) { + const { + isExpired = false, + jtiExist = true, + expExist = true, + vcExist = true, + customVc = undefined, + skipHeader = false, + skipTail = false, + daysAfterExpiration = 0 + } = options; + + const jti = 'foo'; + const releaseTimestamp = Date.parse( releaseDate ); + const day = 86400000; // one day in milliseconds. + + // Depending on isExpired parameter we are creating timestamp ten days + // before or after release day. + const expirationTimestamp = isExpired ? releaseTimestamp - 10 * day : releaseTimestamp + 10 * day; + const todayTimestamp = ( expirationTimestamp + daysAfterExpiration * day ); + + const payload = {}; + + [ 'licensedHosts', 'licenseType', 'usageEndpoint', 'distributionChannel', 'whiteLabel' ].forEach( prop => { + if ( prop in options ) { + payload[ prop ] = options[ prop ]; + } + } ); + + if ( jtiExist ) { + payload.jti = jti; + } + + if ( expExist ) { + payload.exp = Math.ceil( expirationTimestamp / 1000 ); + } + + if ( customVc ) { + payload.vc = customVc; + } else if ( vcExist ) { + const vc = crc32( getCrcInputData( payload ) ); + + payload.vc = vc; + } + + return { + licenseKey: `${ skipHeader ? '' : 'foo.' }${ encodePayload( payload ) }${ skipTail ? '' : '.bar' }`, + todayTimestamp + }; +} + +function encodePayload( claims ) { + return encodeBase64Safe( JSON.stringify( claims ) ); +} + +function encodeBase64Safe( text ) { + return btoa( text ).replace( /\+/g, '-' ).replace( /\//g, '_' ).replace( /=+$/, '' ); +} + +function getCrcInputData( licensePayload ) { + const keysToCheck = Object.getOwnPropertyNames( licensePayload ).sort(); + + const filteredValues = keysToCheck + .filter( key => key != 'vc' && licensePayload[ key ] != null ) + .map( key => licensePayload[ key ] ); + + return [ ...filteredValues ]; +} diff --git a/packages/ckeditor5-core/tests/editor/licensecheck.js b/packages/ckeditor5-core/tests/editor/licensecheck.js new file mode 100644 index 00000000000..2e29c0c9747 --- /dev/null +++ b/packages/ckeditor5-core/tests/editor/licensecheck.js @@ -0,0 +1,706 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals window, console, Response, globalThis, URL */ + +import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror.js'; +import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils.js'; +import Editor from '../../src/editor/editor.js'; +import testUtils from '../../tests/_utils/utils.js'; +import generateKey from '../_utils/generatelicensekey.js'; + +class TestEditor extends Editor { + static create( config ) { + return new Promise( resolve => { + const editor = new this( config ); + + resolve( + editor.initPlugins() + .then( () => { + editor.fire( 'ready' ); + } ) + .then( () => editor ) + ); + } ); + } +} + +describe( 'Editor - license check', () => { + afterEach( () => { + delete TestEditor.builtinPlugins; + delete TestEditor.defaultConfig; + + sinon.restore(); + } ); + + describe( 'license key verification', () => { + let showErrorStub, consoleInfoStub; + + beforeEach( () => { + showErrorStub = testUtils.sinon.stub( TestEditor.prototype, '_showLicenseError' ); + consoleInfoStub = sinon.stub( console, 'info' ); + } ); + + describe( 'required fields in the license key', () => { + it( 'should not block the editor when required fields are provided and are valid', () => { + const { licenseKey } = generateKey(); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + } ); + + it( 'should block the editor when the `exp` field is missing', () => { + const { licenseKey } = generateKey( { expExist: false } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block the editor when the `jti` field is missing', () => { + const { licenseKey } = generateKey( { jtiExist: false } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block the editor when the `vc` field is missing', () => { + const { licenseKey } = generateKey( { vcExist: false } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); + expect( editor.isReadOnly ).to.be.true; + } ); + } ); + + describe( '"licensedHosts" check', () => { + const sets = { + success: [ + { + name: 'direct domain match', + hostname: 'example.com', + licensedHost: 'example.com' + }, + { + name: 'direct IP match', + hostname: '127.0.0.1', + licensedHost: '127.0.0.1' + }, + { + name: 'wildcard IP match', + hostname: '127.0.0.1', + licensedHost: '127.*.*.*' + }, + { + name: 'wildcard subdomain match', + hostname: 'subdomain.example.com', + licensedHost: '*.example.com' + } + ], + fail: [ + { + name: 'domain mismatch', + hostname: 'example.com', + licensedHost: 'example.net' + }, + { + name: 'IP mismatch', + hostname: '127.0.0.1', + licensedHost: '127.0.0.2' + }, + { + name: 'domain mismatch (wildcard subdomain)', + hostname: 'sub.example.com', + licensedHost: '*.example.net' + }, + { + name: 'IP mismatch (wildcard)', + hostname: '127.0.0.1', + licensedHost: '192.168.*.*' + }, + { + name: 'subdomain mismatch', + hostname: 'subdomain.example.com', + licensedHost: 'sub.example.com' + }, + { + name: 'missing root domain', + hostname: 'example.com', + licensedHost: 'subdomain.example.com' + }, + { + name: 'missing subdomain', + hostname: 'subdomain.example.com', + licensedHost: 'example.com' + } + ] + }; + + sets.success.forEach( set => { + it( `works on ${ set.name }`, () => { + sinon.stub( URL.prototype, 'hostname' ).value( set.hostname ); + + const { licenseKey } = generateKey( { licensedHosts: [ set.licensedHost ] } ); + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + + expect( editor.isReadOnly ).to.be.false; + } ); + } ); + + sets.fail.forEach( set => { + it( `fails on ${ set.name }`, () => { + sinon.stub( URL.prototype, 'hostname' ).value( set.hostname ); + + const { licenseKey } = generateKey( { licensedHosts: [ set.licensedHost ] } ); + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'domainLimit' ); + + expect( editor.isReadOnly ).to.be.true; + } ); + } ); + } ); + + describe( 'distribution channel check', () => { + afterEach( () => { + delete window[ Symbol.for( 'cke distribution' ) ]; + } ); + + it( 'should not block if distribution channel match', () => { + setChannel( 'xyz' ); + + const { licenseKey } = generateKey( { distributionChannel: 'xyz' } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + } ); + + it( 'should not block if one of distribution channel match', () => { + setChannel( 'xyz' ); + + const { licenseKey } = generateKey( { distributionChannel: [ 'abc', 'xyz' ] } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + } ); + + it( 'should not block if implicit distribution channel match', () => { + const { licenseKey } = generateKey( { distributionChannel: 'sh' } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + } ); + + it( 'should not block if distribution channel is not restricted', () => { + setChannel( 'xyz' ); + + const { licenseKey } = generateKey(); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + } ); + + it( 'should block if distribution channel doesn\'t match', () => { + setChannel( 'abc' ); + + const { licenseKey } = generateKey( { distributionChannel: 'xyz' } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'distributionChannel' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block if none of distribution channel doesn\'t match', () => { + setChannel( 'abc' ); + + const { licenseKey } = generateKey( { distributionChannel: [ 'xyz', 'def' ] } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'distributionChannel' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block if implicit distribution channel doesn\'t match', () => { + const { licenseKey } = generateKey( { distributionChannel: 'xyz' } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'distributionChannel' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + describe( 'GPL license', () => { + it( 'should block if distribution channel is cloud', () => { + setChannel( 'cloud' ); + + const licenseKey = 'GPL'; + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'distributionChannel' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should not block if distribution channel is not cloud', () => { + setChannel( 'xyz' ); + + const licenseKey = 'GPL'; + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + } ); + } ); + + function setChannel( channel ) { + window[ Symbol.for( 'cke distribution' ) ] = channel; + } + } ); + + describe( 'GPL check', () => { + it( 'should not throw if license key is GPL', () => { + const licenseKey = 'GPL'; + + expect( () => { + // eslint-disable-next-line no-new + new TestEditor( { licenseKey } ); + } ).to.not.throw(); + } ); + + it( 'should not throw if license key is missing (CKEditor testing environment)', () => { + expect( () => { + // eslint-disable-next-line no-new + new TestEditor( {} ); + } ).to.not.throw(); + } ); + + it( 'should throw if license key is missing (outside of CKEditor testing environment)', () => { + window.CKEDITOR_GLOBAL_LICENSE_KEY = undefined; + + expect( () => { + // eslint-disable-next-line no-new + new TestEditor( {} ); + } ).to.throw( CKEditorError, 'editor-license-key-missing' ); + + window.CKEDITOR_GLOBAL_LICENSE_KEY = 'GPL'; + } ); + } ); + + describe( 'evaluation/trial check', () => { + const licenseTypes = [ 'evaluation', 'trial' ]; + + beforeEach( () => { + sinon.useFakeTimers( { now: Date.now() } ); + } ); + + afterEach( () => { + sinon.restore(); + } ); + + licenseTypes.forEach( licenseType => { + it( `should not block if ${ licenseType } license did not expired`, () => { + const { licenseKey, todayTimestamp } = generateKey( { + licenseType, + isExpired: false, + daysAfterExpiration: -1 + } ); + + const today = todayTimestamp; + const dateNow = sinon.stub( Date, 'now' ).returns( today ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + + dateNow.restore(); + } ); + + it( `should block if ${ licenseType } license is expired`, () => { + const { licenseKey, todayTimestamp } = generateKey( { + licenseType, + daysAfterExpiration: 1 + } ); + + const dateNow = sinon.stub( Date, 'now' ).returns( todayTimestamp ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'expired' ); + expect( editor.isReadOnly ).to.be.true; + + dateNow.restore(); + } ); + + it( `should block editor after 10 minutes on ${ licenseType } license`, () => { + const { licenseKey, todayTimestamp } = generateKey( { + licenseType, + isExpired: false, + daysAfterExpiration: -1 + } ); + + const dateNow = sinon.stub( Date, 'now' ).returns( todayTimestamp ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + + sinon.clock.tick( 600100 ); + + sinon.assert.calledWithMatch( showErrorStub, licenseType + 'Limit' ); + expect( editor.isReadOnly ).to.be.true; + sinon.assert.calledOnce( consoleInfoStub ); + sinon.assert.calledWith( + consoleInfoStub, + `You are using the ${ licenseType } version of CKEditor 5 with limited usage. ` + + 'Make sure you will not use it in the production environment.' + ); + + dateNow.restore(); + } ); + + it( `should clear timer on editor destroy on ${ licenseType } license`, done => { + const { licenseKey, todayTimestamp } = generateKey( { + licenseType, + isExpired: false, + daysAfterExpiration: -1 + } ); + + const dateNow = sinon.stub( Date, 'now' ).returns( todayTimestamp ); + const editor = new TestEditor( { licenseKey } ); + const clearTimeoutSpy = sinon.spy( globalThis, 'clearTimeout' ); + + editor.fire( 'ready' ); + editor.on( 'destroy', () => { + sinon.assert.calledOnce( clearTimeoutSpy ); + done(); + } ); + + editor.destroy(); + dateNow.restore(); + } ); + } ); + } ); + + describe( 'development license', () => { + beforeEach( () => { + sinon.useFakeTimers( { now: Date.now() } ); + } ); + + afterEach( () => { + sinon.restore(); + } ); + + it( 'should log information to the console about using the development license', () => { + const { licenseKey } = generateKey( { + licenseType: 'development' + } ); + + const editor = new TestEditor( { licenseKey } ); + + expect( editor.isReadOnly ).to.be.false; + sinon.assert.calledOnce( consoleInfoStub ); + sinon.assert.calledWith( consoleInfoStub, 'You are using the development version of CKEditor 5 with ' + + 'limited usage. Make sure you will not use it in the production environment.' ); + } ); + + it( 'should not block the editor if 10 minutes have not passed (development license)', () => { + const { licenseKey } = generateKey( { + licenseType: 'development' + } ); + + const today = 1715166436000; // 08.05.2024 + const dateNow = sinon.stub( Date, 'now' ).returns( today ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + + sinon.clock.tick( 1 ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + + dateNow.restore(); + } ); + + it( 'should block editor after 10 minutes (development license)', () => { + const { licenseKey, todayTimestamp } = generateKey( { + licenseType: 'development' + } ); + + const dateNow = sinon.stub( Date, 'now' ).returns( todayTimestamp ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + + sinon.clock.tick( 600100 ); + + sinon.assert.calledWithMatch( showErrorStub, 'developmentLimit' ); + expect( editor.isReadOnly ).to.be.true; + + dateNow.restore(); + } ); + + it( 'should clear timer on editor destroy', done => { + const { licenseKey, todayTimestamp } = generateKey( { + licenseType: 'development' + } ); + + const dateNow = sinon.stub( Date, 'now' ).returns( todayTimestamp ); + const editor = new TestEditor( { licenseKey } ); + const clearTimeoutSpy = sinon.spy( globalThis, 'clearTimeout' ); + + editor.fire( 'ready' ); + editor.on( 'destroy', () => { + sinon.assert.calledOnce( clearTimeoutSpy ); + done(); + } ); + + editor.destroy(); + dateNow.restore(); + } ); + } ); + + it( 'should block the editor when the license key is not valid (expiration date in the past)', () => { + const { licenseKey } = generateKey( { + isExpired: true + } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'expired' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block the editor when the license key has wrong format (wrong verificationCode)', () => { + const { licenseKey } = generateKey( { + customVc: 'wrong vc' + } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block the editor when the license key has wrong format (missing header part)', () => { + const { licenseKey } = generateKey( { + isExpired: true, + skipHeader: true + } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block the editor when the license key has wrong format (missing tail part)', () => { + const { licenseKey } = generateKey( { + isExpired: true, + skipTail: true + } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block the editor when the license key has wrong format (payload does not start with `ey`)', () => { + const licenseKey = 'foo.JleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZvbyIsInZlcmlmaWNhdGlvbkNvZGUiOiJjNTU2YWQ3NCJ9.bar'; + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block the editor when the license key has wrong format (payload not parsable as a JSON object)', () => { + const licenseKey = 'foo.eyZm9v.bar'; + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); + expect( editor.isReadOnly ).to.be.true; + } ); + } ); + + describe( 'usage endpoint', () => { + it( 'should send request with telemetry data if license key contains a usage endpoint', () => { + const fetchStub = sinon.stub( window, 'fetch' ); + + const { licenseKey } = generateKey( { + usageEndpoint: 'https://ckeditor.com' + } ); + const editor = new TestEditor( { licenseKey } ); + + editor.fire( 'ready' ); + + sinon.assert.calledOnce( fetchStub ); + + const sentData = JSON.parse( fetchStub.firstCall.lastArg.body ); + + expect( sentData.license ).to.equal( licenseKey ); + expect( sentData.telemetry ).to.deep.equal( { editorVersion: globalThis.CKEDITOR_VERSION } ); + } ); + + it( 'should not send any request if license key does not contain a usage endpoint', () => { + const fetchStub = sinon.stub( window, 'fetch' ); + + const { licenseKey } = generateKey(); + const editor = new TestEditor( { licenseKey } ); + + editor.fire( 'ready' ); + + sinon.assert.notCalled( fetchStub ); + } ); + + it( 'should display error on the console and not block the editor if response status is not ok (HTTP 500)', async () => { + const { licenseKey } = generateKey( { + usageEndpoint: 'https://ckeditor.com' + } ); + const fetchStub = sinon.stub( window, 'fetch' ).resolves( new Response( null, { status: 500 } ) ); + const errorStub = sinon.stub( console, 'error' ); + + const editor = new TestEditor( { licenseKey } ); + + editor.fire( 'ready' ); + await wait( 1 ); + + sinon.assert.calledOnce( fetchStub ); + sinon.assert.calledWithMatch( + errorStub, 'license-key-validation-endpoint-not-reachable', { 'url': 'https://ckeditor.com' } ); + expect( editor.isReadOnly ).to.be.false; + } ); + + it( 'should display warning and block the editor when usage status is not ok', async () => { + const fetchStub = sinon.stub( window, 'fetch' ).resolves( { + ok: true, + json: () => Promise.resolve( { + status: 'foo' + } ) + } ); + const showErrorStub = testUtils.sinon.stub( TestEditor.prototype, '_showLicenseError' ); + + const { licenseKey } = generateKey( { + usageEndpoint: 'https://ckeditor.com' + } ); + const editor = new TestEditor( { licenseKey } ); + + editor.fire( 'ready' ); + await wait( 1 ); + + sinon.assert.calledOnce( fetchStub ); + sinon.assert.calledOnce( showErrorStub ); + sinon.assert.calledWithMatch( showErrorStub, 'usageLimit' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should display additional warning when usage status is not ok and message is provided', async () => { + const fetchStub = sinon.stub( window, 'fetch' ).resolves( { + ok: true, + json: () => Promise.resolve( { + status: 'foo', + message: 'bar' + } ) + } ); + const warnStub = testUtils.sinon.stub( console, 'warn' ); + const showErrorStub = testUtils.sinon.stub( TestEditor.prototype, '_showLicenseError' ); + + const { licenseKey } = generateKey( { + usageEndpoint: 'https://ckeditor.com' + } ); + const editor = new TestEditor( { licenseKey } ); + + editor.fire( 'ready' ); + await wait( 1 ); + + sinon.assert.calledOnce( fetchStub ); + sinon.assert.calledOnce( warnStub ); + sinon.assert.calledOnce( showErrorStub ); + sinon.assert.calledWithMatch( warnStub, 'bar' ); + sinon.assert.calledWithMatch( showErrorStub, 'usageLimit' ); + expect( editor.isReadOnly ).to.be.true; + } ); + } ); + + describe( 'license errors', () => { + let clock; + + beforeEach( () => { + clock = sinon.useFakeTimers( { toFake: [ 'setTimeout' ] } ); + } ); + + const testCases = [ + { reason: 'invalid', error: 'invalid-license-key' }, + { reason: 'expired', error: 'license-key-expired' }, + { reason: 'domainLimit', error: 'license-key-domain-limit' }, + { reason: 'featureNotAllowed', error: 'license-key-feature-not-allowed', pluginName: 'PluginABC' }, + { reason: 'evaluationLimit', error: 'license-key-evaluation-limit' }, + { reason: 'trialLimit', error: 'license-key-trial-limit' }, + { reason: 'developmentLimit', error: 'license-key-development-limit' }, + { reason: 'usageLimit', error: 'license-key-usage-limit' }, + { reason: 'distributionChannel', error: 'license-key-distribution-channel' } + ]; + + for ( const testCase of testCases ) { + const { reason, error, pluginName } = testCase; + const expectedData = pluginName ? { pluginName } : undefined; + + it( `should throw \`${ error }\` error`, () => { + const editor = new TestEditor( { licenseKey: 'GPL' } ); + + editor._showLicenseError( reason, pluginName ); + + expectToThrowCKEditorError( () => clock.tick( 1 ), error, editor, expectedData ); + } ); + } + + it( 'should throw error only once', () => { + const editor = new TestEditor( { licenseKey: 'GPL' } ); + + editor._showLicenseError( 'invalid' ); + + try { + clock.tick( 1 ); + } catch ( e ) { + // Do nothing. + } + + editor._showLicenseError( 'invalid' ); + + expect( () => clock.tick( 1 ) ).to.not.throw(); + } ); + } ); +} ); + +function wait( time ) { + return new Promise( res => { + window.setTimeout( res, time ); + } ); +} diff --git a/packages/ckeditor5-easy-image/ckeditor5-metadata.json b/packages/ckeditor5-easy-image/ckeditor5-metadata.json index 260bf92da1f..ecf163ecac5 100644 --- a/packages/ckeditor5-easy-image/ckeditor5-metadata.json +++ b/packages/ckeditor5-easy-image/ckeditor5-metadata.json @@ -4,7 +4,6 @@ "name": "Easy Image", "className": "EasyImage", "description": "An image upload tool with virtually zero server setup. The images are automatically rescaled, optimized, responsive and delivered through a CDN.", - "docs": "features/images/image-upload/easy-image.html", "path": "src/easyimage.js", "requires": [ "CloudServices", diff --git a/packages/ckeditor5-easy-image/docs/_snippets/features/easy-image.js b/packages/ckeditor5-easy-image/docs/_snippets/features/easy-image.js index 974f7760718..51108cd2c98 100644 --- a/packages/ckeditor5-easy-image/docs/_snippets/features/easy-image.js +++ b/packages/ckeditor5-easy-image/docs/_snippets/features/easy-image.js @@ -14,7 +14,8 @@ ClassicEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-easy-image/docs/api/easy-image.md b/packages/ckeditor5-easy-image/docs/api/easy-image.md index 76e2ec9c6ae..c2ea5318fd3 100644 --- a/packages/ckeditor5-easy-image/docs/api/easy-image.md +++ b/packages/ckeditor5-easy-image/docs/api/easy-image.md @@ -6,13 +6,13 @@ category: api-reference [![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-easy-image.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-easy-image) -This package implements the {@link features/easy-image Easy Image} feature for CKEditor 5. +This package implements the Easy Image feature for CKEditor 5. Easy Image lets you easily insert images which are automatically rescaled, optimized, responsive and delivered through a blazing-fast CDN. It integrates automatically with the [CKEditor Cloud Services](https://ckeditor.com/ckeditor-cloud-services/). ## Documentation -See the {@link features/easy-image "Easy Image integration" guide} and the {@link module:easy-image/easyimage~EasyImage plugin documentation} to learn how to enable the integration. +See the {@link module:easy-image/easyimage~EasyImage plugin documentation} to learn how to enable the integration. Check out the {@link features/image-upload comprehensive "Image upload" guide} to learn about other ways to upload images into CKEditor 5. diff --git a/packages/ckeditor5-easy-image/docs/features/easy-image.md b/packages/ckeditor5-easy-image/docs/features/easy-image.md deleted file mode 100644 index aa91b5a4927..00000000000 --- a/packages/ckeditor5-easy-image/docs/features/easy-image.md +++ /dev/null @@ -1,167 +0,0 @@ ---- -category: features-image-upload -menu-title: Easy Image -meta-title: Easy Image | CKEditor 5 Documentation -order: 30 -badges: [ premium ] ---- - -# Easy Image integration - -The [Easy Image](https://ckeditor.com/ckeditor-cloud-services/easy-image/) is an intuitive tool for uploading images. Unlike the {@link features/ckbox CKBox} feature, which is a full-fledged file manager, Easy Image concentrates on upload only. - - - This is a premium feature and you need a license for it on top of your CKEditor 5 commercial license. [Contact us](https://ckeditor.com/contact/?sales=true#contact-form) to receive an offer tailored to your needs. - - You can also sign up for the [CKEditor Premium Features 30-day free trial](https://orders.ckeditor.com/trial/premium-features) to test the feature. - - -## Demo - -The demo below uses the classic editor type. It is configured to use the Easy Image service provided by CKEditor Cloud Services. - -{@snippet build-classic-source} - -{@snippet features/easy-image} - - - This demo presents a limited set of features. Visit the {@link examples/builds/full-featured-editor feature-rich editor example} to see more in action. - - -## Additional feature information - -Easy Image is part of the CKEditor Cloud Services. It is a SaaS product which: - -* securely uploads images, -* takes care of rescaling and [optimizing them](https://ckeditor.com/docs/cs/latest/guides/easy-image/service-details.html#image-processing) as well as providing [various image sizes](#responsive-images) (responsive images), -* delivers uploaded images through a blazing-fast CDN. - -All that with virtually zero server setup. - -## Configuration - -To make enabling image upload in CKEditor 5 a breeze, the {@link module:easy-image/easyimage~EasyImage `EasyImage` plugin} integrates with the Easy Image service provided by [CKEditor Cloud Services](https://ckeditor.com/ckeditor-cloud-services/). Enabling it is straightforward and the results are immediate: - -1. Follow the [Easy Image – Quick start guide](https://ckeditor.com/docs/cs/latest/guides/easy-image/quick-start.html) to set up an account. -2. Configure CKEditor 5 (see {@link module:cloud-services/cloudservicesconfig~CloudServicesConfig `CloudServicesConfig`}): - -```js -ClassicEditor - .create( document.querySelector( '#editor' ), { - cloudServices: { - tokenUrl: 'https://example.com/cs-token-endpoint', - uploadUrl: 'https://your-organization-id.cke-cs.com/easyimage/upload/' - } - } ) - .then( /* ... */ ) - .catch( /* ... */ ); -``` - -This is all. At this point, image upload will be automatically enabled in your application. - -If you are having trouble with setting up Easy Image, please [contact us](https://ckeditor.com/contact/). - -### Configuring allowed file types - -The allowed file types that can be uploaded should actually be configured in two places: - -* On the client side, in CKEditor 5, restricting image upload through the CKEditor 5 UI and commands. -* On the server side, in Easy Image, restricting the file formats that are accepted in Easy Image. - -#### Client-side configuration - -Use the {@link module:image/imageconfig~ImageUploadConfig#types `image.upload.types`} configuration option to define the allowed image MIME types that can be uploaded to CKEditor 5. - -By default, users are allowed to upload `jpeg`, `png`, `gif`, `bmp`, `webp` and `tiff` files. This corresponds with file formats supported by Easy Image, but you can modify the list to limit the number of allowed image types. - -#### Server-side configuration - -Check the [list of file formats supported by Easy Image](https://ckeditor.com/docs/cs/latest/guides/easy-image/service-details.html#supported-file-formats). At the moment it is not possible to limit or extend this list so any restrictions need to be introduced on the client side. - -## Responsive images - -Another great feature introduced with CKEditor 5 is the ability to have responsive images in the rich-text editor content. With a single image upload, several optimized versions of that image are created, each for a different size of the display. All this is transparent to the end user who uploaded the image. - -{@img assets/img/responsive-images.svg 550 The visualization of the responsive images approach for CKEditor 5 WYSIWYG editor.} - -### Why responsive images? - -Responsive images have two main advantages over the "traditional" image delivery: - -* **They save the data transfer**. There are countless device and screen size combinations that can be used to display images in your application (smartphones, tablets, laptops, etc.). You do not need to serve the same full–scale images to all of them, though. - - Using Easy Image guarantees only the particular size variant corresponding to the user's screen size is served, minimizing the amount of data transferred to the client. For large images, this can save up to 90% of the transferred data – [see it yourself!](https://ckeditor.com/ckeditor-cloud-services/easy-image/) -* **They load faster**. Because only the image matching the size of the screen is transferred, in most cases it can be loaded and displayed much faster than a "regular" full–scale image. The faster it loads, the sooner the users can see it, which greatly improves the user experience of your application. You no longer need to wait ages for high–resolution photos to load on a tiny smartphone screen. - -### Responsive images in the markup - -Responsive images delivered by the Easy Image service are transparent to your application. Once uploaded, the image appears in the editor content as a "regular" image but with some additional attributes like the `srcset`. - -The `srcset` attribute specifies the image variants dedicated for the various screen sizes for the web browser to choose from (360px, 720px, 1080px, 1440px, etc.). For instance, the `image.jpg` file uploaded by the user will have the following markup: - -```html -
- -
...
-
-``` - -The variety of the image sizes in the `srcset` attribute allows the web browser to choose the best one for the particular screen size. As a result, it loads faster and with less data transferred. See the detailed [Easy Image service documentation](https://ckeditor.com/docs/cs/latest/guides/easy-image/service-details.html) to learn more about responsive images and other features offered by the service. - -## Installation - - - ⚠️ **New import paths** - - Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. - - -This package is part of our open-source aggregate package. - -```bash -npm install ckeditor5 -``` - -Then add {@link module:easy-image/easyimage~EasyImage} to your plugin list and [configure](#configuration) the feature. For instance: - -```js -import { EasyImage, Image } from 'ckeditor5'; - -ClassicEditor - .create( document.querySelector( '#editor' ), { - plugins: [ EasyImage, Image, /* ... */ ], - toolbar: [ 'uploadImage', /* ... */ ], - - // Configure the endpoint. See the "Configuration" section above. - cloudServices: { - tokenUrl: 'https://example.com/cs-token-endpoint', - uploadUrl: 'https://your-organization-id.cke-cs.com/easyimage/upload/' - } - } ) - .then( /* ... */ ) - .catch( /* ... */ ); -``` - - - Please note that most integrations will also require the {@link module:image/image~Image} plugin to be loaded in the editor to make this feature work properly (or one of {@link module:image/imageblock~ImageBlock} or {@link module:image/imageinline~ImageInline}). Check out the comprehensive {@link features/images-installation guide to images} in CKEditor 5 to learn more. - - -## What's next - -Check out the comprehensive {@link features/image-upload Image upload overview} to learn more about different ways of uploading images in CKEditor 5. - -See the {@link features/images-overview Image feature} guide to find out more about handling images in CKEditor 5. - -## Contribute - -The source code of the feature is available on GitHub at [https://github.com/ckeditor/ckeditor5/tree/master/packages/ckeditor5-easy-image](https://github.com/ckeditor/ckeditor5/tree/master/packages/ckeditor5-easy-image). diff --git a/packages/ckeditor5-easy-image/src/easyimage.ts b/packages/ckeditor5-easy-image/src/easyimage.ts index 64d9512ee55..064c42e337c 100644 --- a/packages/ckeditor5-easy-image/src/easyimage.ts +++ b/packages/ckeditor5-easy-image/src/easyimage.ts @@ -26,7 +26,7 @@ import CloudServicesUploadAdapter from './cloudservicesuploadadapter.js'; * * {@link module:image/image~Image}, * * {@link module:image/imageupload~ImageUpload}, * - * See the {@glink features/images/image-upload/easy-image "Easy Image integration" guide} to learn how to configure + * See the [Easy Image Quick Start guide](https://ckeditor.com/docs/cs/latest/guides/easy-image/quick-start.html) to learn how to configure * and use this feature. * * Check out the {@glink features/images/image-upload/image-upload comprehensive "Image upload" guide} to learn about diff --git a/packages/ckeditor5-editor-decoupled/docs/framework/document-editor.md b/packages/ckeditor5-editor-decoupled/docs/framework/document-editor.md index 8ce56387a85..e09e339d7f2 100644 --- a/packages/ckeditor5-editor-decoupled/docs/framework/document-editor.md +++ b/packages/ckeditor5-editor-decoupled/docs/framework/document-editor.md @@ -42,10 +42,6 @@ DecoupledEditor.create( document.querySelector( '.document-editor__editable' ), You may have noticed that you have to make sure the editor UI is injected into your application after it fires the {@link module:ui/editorui/editorui~EditorUI#event:ready `EditorUI#ready`} event. The toolbar element can be found under `editor.ui.view.toolbar.element`. - - Document editor supports the Easy Image plugin provided by [CKEditor Cloud Services](https://ckeditor.com/ckeditor-cloud-services/) out of the box. Please refer to the {@link features/easy-image Easy Image documentation} to learn more. - - ## The user interface The code you have just created will run the editor but the user interface is still missing. Start off with a basic HTML structure to host the editor components (the toolbar and the editable). diff --git a/packages/ckeditor5-engine/docs/_snippets/framework/element-reconversion-demo.js b/packages/ckeditor5-engine/docs/_snippets/framework/element-reconversion-demo.js index 5db10c14ec5..e00cd10bea9 100644 --- a/packages/ckeditor5-engine/docs/_snippets/framework/element-reconversion-demo.js +++ b/packages/ckeditor5-engine/docs/_snippets/framework/element-reconversion-demo.js @@ -349,7 +349,8 @@ ClassicEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-external-link-target.js b/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-external-link-target.js index 1786b61205a..7408808b804 100644 --- a/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-external-link-target.js +++ b/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-external-link-target.js @@ -39,7 +39,8 @@ ClassicEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-heading-class.js b/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-heading-class.js index c52d1dc994c..f4d67a13563 100644 --- a/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-heading-class.js +++ b/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-heading-class.js @@ -25,7 +25,8 @@ ClassicEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-link-class.js b/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-link-class.js index 77bbaeb4e3f..bc38e45008a 100644 --- a/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-link-class.js +++ b/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-link-class.js @@ -35,7 +35,8 @@ ClassicEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-unsafe-link-class.js b/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-unsafe-link-class.js index 5b2d5274e32..f2bdc994ff7 100644 --- a/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-unsafe-link-class.js +++ b/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-unsafe-link-class.js @@ -35,7 +35,8 @@ ClassicEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-div-attributes.js b/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-div-attributes.js index 9ad3513fec2..34b19c81d6f 100644 --- a/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-div-attributes.js +++ b/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-div-attributes.js @@ -66,7 +66,8 @@ ClassicEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-link-target.js b/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-link-target.js index 83b1676bbb8..5c81d2dff50 100644 --- a/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-link-target.js +++ b/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-link-target.js @@ -39,7 +39,8 @@ ClassicEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-arbitrary-attribute-values.js b/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-arbitrary-attribute-values.js index 631a8bee3e8..e65a0d8eab0 100644 --- a/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-arbitrary-attribute-values.js +++ b/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-arbitrary-attribute-values.js @@ -62,7 +62,8 @@ ClassicEditor }, fontSize: { options: [ 10, 12, 14, 'default', 18, 20, 22 ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-element-converter.js b/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-element-converter.js index d6482589f43..fe0b4ea3dd7 100644 --- a/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-element-converter.js +++ b/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-element-converter.js @@ -198,7 +198,8 @@ ClassicEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-figure-attributes.js b/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-figure-attributes.js index 9b0acfa8d83..8f59c8f1c57 100644 --- a/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-figure-attributes.js +++ b/packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-figure-attributes.js @@ -180,7 +180,8 @@ ClassicEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-heading-interactive.js b/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-heading-interactive.js index 8782018a324..65345df1f89 100644 --- a/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-heading-interactive.js +++ b/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-heading-interactive.js @@ -47,7 +47,8 @@ function CustomHeading( editor ) { DecoupledEditor.create( document.querySelector( '#mini-inspector-heading-interactive' ), { plugins: [ Essentials, CustomHeading ], - toolbar: [] + toolbar: [], + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-heading.js b/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-heading.js index 7287ec4e08a..fef1c50e728 100644 --- a/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-heading.js +++ b/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-heading.js @@ -19,7 +19,8 @@ function CustomHeading( editor ) { DecoupledEditor.create( document.querySelector( '#mini-inspector-heading' ), { plugins: [ Essentials, CustomHeading ], - toolbar: [] + toolbar: [], + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-structure.js b/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-structure.js index 4c546cae69d..da65fcdfec5 100644 --- a/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-structure.js +++ b/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-structure.js @@ -91,7 +91,8 @@ function Structure( editor ) { DecoupledEditor.create( document.querySelector( '#mini-inspector-structure' ), { plugins: [ Essentials, Paragraph, Structure ], - toolbar: [] + toolbar: [], + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-upcast-attribute.js b/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-upcast-attribute.js index 2ba929ddc05..e38ad16f91b 100644 --- a/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-upcast-attribute.js +++ b/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-upcast-attribute.js @@ -27,7 +27,8 @@ function Image( editor ) { DecoupledEditor.create( document.querySelector( '#mini-inspector-upcast-attribute' ), { plugins: [ Essentials, Image ], - toolbar: [] + toolbar: [], + licenseKey: 'GPL' } ) .then( editor => { MiniCKEditorInspector.attach( diff --git a/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-upcast-element.js b/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-upcast-element.js index fed4b4e9591..0e1037f0869 100644 --- a/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-upcast-element.js +++ b/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector-upcast-element.js @@ -21,7 +21,8 @@ function Example( editor ) { DecoupledEditor.create( document.querySelector( '#mini-inspector-upcast-element' ), { plugins: [ Essentials, Example ], - toolbar: [] + toolbar: [], + licenseKey: 'GPL' } ) .then( editor => { MiniCKEditorInspector.attach( diff --git a/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector.js b/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector.js index a7c25c00b53..130454538e1 100644 --- a/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector.js +++ b/packages/ckeditor5-engine/docs/_snippets/framework/mini-inspector.js @@ -131,7 +131,8 @@ DecoupledEditor.defaultConfig = { } }, // This value must be kept in sync with the language defined in webpack.config.js. - language: 'en' + language: 'en', + licenseKey: 'GPL' }; window.DecoupledEditor = DecoupledEditor; diff --git a/packages/ckeditor5-find-and-replace/docs/_snippets/features/find-and-replace-dropdown.js b/packages/ckeditor5-find-and-replace/docs/_snippets/features/find-and-replace-dropdown.js index 42d427b4e08..9a84e8e3cf1 100644 --- a/packages/ckeditor5-find-and-replace/docs/_snippets/features/find-and-replace-dropdown.js +++ b/packages/ckeditor5-find-and-replace/docs/_snippets/features/find-and-replace-dropdown.js @@ -37,7 +37,8 @@ ClassicEditor }, findAndReplace: { uiType: 'dropdown' - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editorWithDropdown = editor; diff --git a/packages/ckeditor5-find-and-replace/docs/_snippets/features/find-and-replace.js b/packages/ckeditor5-find-and-replace/docs/_snippets/features/find-and-replace.js index b11c7ed89fa..3f57fec1067 100644 --- a/packages/ckeditor5-find-and-replace/docs/_snippets/features/find-and-replace.js +++ b/packages/ckeditor5-find-and-replace/docs/_snippets/features/find-and-replace.js @@ -34,7 +34,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-find-and-replace/docs/features/find-and-replace.md b/packages/ckeditor5-find-and-replace/docs/features/find-and-replace.md index d8991d35151..2d77fd562a2 100644 --- a/packages/ckeditor5-find-and-replace/docs/features/find-and-replace.md +++ b/packages/ckeditor5-find-and-replace/docs/features/find-and-replace.md @@ -27,15 +27,19 @@ Use the find and replace toolbar button {@icon @ckeditor/ckeditor5-find-and-repl Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, FindAndReplace } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ FindAndReplace, /* ... */ ], toolbar: [ 'findAndReplace', /* ... */ ], + findAndReplace: { + // Configuration. + } } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -50,6 +54,7 @@ By default, the find and replace form displays inside a dialog. That allows for ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... findAndReplace: { uiType: 'dropdown' } diff --git a/packages/ckeditor5-font/docs/_snippets/features/custom-font-color-and-background-color-options.js b/packages/ckeditor5-font/docs/_snippets/features/custom-font-color-and-background-color-options.js index a1be15637ee..122d22a4cac 100644 --- a/packages/ckeditor5-font/docs/_snippets/features/custom-font-color-and-background-color-options.js +++ b/packages/ckeditor5-font/docs/_snippets/features/custom-font-color-and-background-color-options.js @@ -227,7 +227,8 @@ ClassicEditor colorPicker: { format: 'hex' } - } + }, + licenseKey: 'GPL' } ) .then( editor => { if ( !window.editors ) { diff --git a/packages/ckeditor5-font/docs/_snippets/features/custom-font-family-options.js b/packages/ckeditor5-font/docs/_snippets/features/custom-font-family-options.js index a76c6f2dd5c..0f33ddcf92c 100644 --- a/packages/ckeditor5-font/docs/_snippets/features/custom-font-family-options.js +++ b/packages/ckeditor5-font/docs/_snippets/features/custom-font-family-options.js @@ -43,7 +43,8 @@ ClassicEditor 'Ubuntu, Arial, sans-serif', 'Ubuntu Mono, Courier New, Courier, monospace' ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-font/docs/_snippets/features/custom-font-size-named-options.js b/packages/ckeditor5-font/docs/_snippets/features/custom-font-size-named-options.js index cdc52d9704b..5a939a38c76 100644 --- a/packages/ckeditor5-font/docs/_snippets/features/custom-font-size-named-options.js +++ b/packages/ckeditor5-font/docs/_snippets/features/custom-font-size-named-options.js @@ -44,7 +44,8 @@ ClassicEditor 'default', 'big' ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-font/docs/_snippets/features/custom-font-size-numeric-options.js b/packages/ckeditor5-font/docs/_snippets/features/custom-font-size-numeric-options.js index 0e945543bd3..87c20889257 100644 --- a/packages/ckeditor5-font/docs/_snippets/features/custom-font-size-numeric-options.js +++ b/packages/ckeditor5-font/docs/_snippets/features/custom-font-size-numeric-options.js @@ -48,7 +48,8 @@ ClassicEditor 19, 21 ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-font/docs/_snippets/features/font.js b/packages/ckeditor5-font/docs/_snippets/features/font.js index f24dfbd40d5..0e12d9a218f 100644 --- a/packages/ckeditor5-font/docs/_snippets/features/font.js +++ b/packages/ckeditor5-font/docs/_snippets/features/font.js @@ -35,7 +35,8 @@ ClassicEditor ckbox: { tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-font/docs/features/font.md b/packages/ckeditor5-font/docs/features/font.md index f011b28787f..978d9102c48 100644 --- a/packages/ckeditor5-font/docs/features/font.md +++ b/packages/ckeditor5-font/docs/features/font.md @@ -40,15 +40,23 @@ The plugin enables the following features in the rich-text editor: Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, Font } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Font, /* ... */ ], toolbar: [ 'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor', /* ... */ ] + fontFamily: { + // Configuration. + } + fontCOlor: { + // Configuration. + } + // ... } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -79,6 +87,7 @@ For example, the following editor supports two font families besides the default ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... fontFamily: { options: [ 'default', @@ -103,6 +112,7 @@ By default, all `font-family` values that are not specified in the `config.fontF ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... fontFamily: { options: [ // Font family configuration options are described in the "Configuring the font family feature" section. @@ -167,6 +177,7 @@ An example of an editor that supports two font sizes: ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... fontSize: { options: [ 'tiny', @@ -199,6 +210,7 @@ Here is an example of the WYSIWYG editor that supports numerical font sizes. Not ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... fontSize: { options: [ 9, @@ -227,6 +239,7 @@ By default, all `font-size` values that are not specified in the `config.fontSiz ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... fontSize: { options: [ // Numerical values. @@ -264,6 +277,7 @@ It is possible to configure which colors are available in the color dropdown. Us ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... fontColor: { colors: [ { @@ -334,6 +348,7 @@ Usually, you will want to use this option when changing the number of [available ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... fontColor: { colors: [ // 9 colors defined here. @@ -367,6 +382,7 @@ By default, the number of displayed document colors is limited to one row, but y ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... fontColor: { // Display 6 columns in the color grid. columns: 6, @@ -403,6 +419,7 @@ To turn off the color picker entirely for the given feature, set the {@link modu ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... fontColor: { colorPicker: { // Use 'hex' format for output instead of 'hsl'. diff --git a/packages/ckeditor5-heading/docs/_snippets/features/custom-heading-buttons.js b/packages/ckeditor5-heading/docs/_snippets/features/custom-heading-buttons.js index d07a5c357a5..ccb11f59821 100644 --- a/packages/ckeditor5-heading/docs/_snippets/features/custom-heading-buttons.js +++ b/packages/ckeditor5-heading/docs/_snippets/features/custom-heading-buttons.js @@ -51,7 +51,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-heading/docs/_snippets/features/custom-heading-elements.js b/packages/ckeditor5-heading/docs/_snippets/features/custom-heading-elements.js index 27881cbe1a5..46f342ebcd0 100644 --- a/packages/ckeditor5-heading/docs/_snippets/features/custom-heading-elements.js +++ b/packages/ckeditor5-heading/docs/_snippets/features/custom-heading-elements.js @@ -57,7 +57,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-heading/docs/_snippets/features/custom-heading-levels.js b/packages/ckeditor5-heading/docs/_snippets/features/custom-heading-levels.js index 4afb90f0381..b8741b221e0 100644 --- a/packages/ckeditor5-heading/docs/_snippets/features/custom-heading-levels.js +++ b/packages/ckeditor5-heading/docs/_snippets/features/custom-heading-levels.js @@ -47,7 +47,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-heading/docs/_snippets/features/default-headings.js b/packages/ckeditor5-heading/docs/_snippets/features/default-headings.js index 0c293a560d8..a2df322276f 100644 --- a/packages/ckeditor5-heading/docs/_snippets/features/default-headings.js +++ b/packages/ckeditor5-heading/docs/_snippets/features/default-headings.js @@ -48,7 +48,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-heading/docs/_snippets/features/heading-buttons.js b/packages/ckeditor5-heading/docs/_snippets/features/heading-buttons.js index a8beea0f138..1b06b57e4ff 100644 --- a/packages/ckeditor5-heading/docs/_snippets/features/heading-buttons.js +++ b/packages/ckeditor5-heading/docs/_snippets/features/heading-buttons.js @@ -48,7 +48,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-heading/docs/_snippets/features/title.js b/packages/ckeditor5-heading/docs/_snippets/features/title.js index 7e7b28f640b..d751dbb255e 100644 --- a/packages/ckeditor5-heading/docs/_snippets/features/title.js +++ b/packages/ckeditor5-heading/docs/_snippets/features/title.js @@ -105,7 +105,8 @@ BalloonEditor.defaultConfig = { ] }, // This value must be kept in sync with the language defined in webpack.config.js. - language: 'en' + language: 'en', + licenseKey: 'GPL' }; BalloonEditor.builtinPlugins.push( Title ); @@ -133,7 +134,8 @@ BalloonEditor 'blockQuote', 'insertTable', 'mediaEmbed' - ] + ], + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-heading/docs/features/headings.md b/packages/ckeditor5-heading/docs/features/headings.md index aec5d8c8c80..d00cf72e6ab 100644 --- a/packages/ckeditor5-heading/docs/features/headings.md +++ b/packages/ckeditor5-heading/docs/features/headings.md @@ -47,15 +47,19 @@ The heading feature lets you also use a set of heading buttons instead of the dr Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, Heading } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Heading, /* ... */ ], toolbar: [ 'heading', /* ... */ ] + heading: { + // Configuration. + } } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -88,6 +92,7 @@ For example, the following editor will support only two levels of headings &ndas ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... heading: { options: [ { model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' }, @@ -128,6 +133,7 @@ For example, the following editor will support the following two heading options ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... heading: { options: [ { model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' }, @@ -161,6 +167,7 @@ To use individual toolbar buttons instead of the heading dropdown, you need to p ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... toolbar: [ 'paragraph', 'heading1', 'heading2', 'heading3', 'heading4', 'heading5', 'heading6', '|', 'undo', 'redo' ], heading: { options: [ diff --git a/packages/ckeditor5-heading/docs/features/title.md b/packages/ckeditor5-heading/docs/features/title.md index cf87732463c..aff9e4b43bb 100644 --- a/packages/ckeditor5-heading/docs/features/title.md +++ b/packages/ckeditor5-heading/docs/features/title.md @@ -30,14 +30,18 @@ The title plugin lets you move from the title to the body element using the -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, Title } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Title, /* ... */ ] + title: { + // Configuration. + } } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -52,7 +56,7 @@ To change the title placeholder, use the {@link module:heading/title~TitleConfig ```js ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ Title, /* ... */ ], + // ... Other configuration options ... title: { placeholder: 'My custom placeholder for the title' }, diff --git a/packages/ckeditor5-highlight/docs/_snippets/features/custom-highlight-colors-inline.js b/packages/ckeditor5-highlight/docs/_snippets/features/custom-highlight-colors-inline.js index 2bc859f3cd7..bd9e1dac036 100644 --- a/packages/ckeditor5-highlight/docs/_snippets/features/custom-highlight-colors-inline.js +++ b/packages/ckeditor5-highlight/docs/_snippets/features/custom-highlight-colors-inline.js @@ -59,7 +59,8 @@ ClassicEditor type: 'pen' } ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-highlight/docs/_snippets/features/custom-highlight-colors-variables.js b/packages/ckeditor5-highlight/docs/_snippets/features/custom-highlight-colors-variables.js index 2f23b33a289..7dcac3e1abf 100644 --- a/packages/ckeditor5-highlight/docs/_snippets/features/custom-highlight-colors-variables.js +++ b/packages/ckeditor5-highlight/docs/_snippets/features/custom-highlight-colors-variables.js @@ -59,7 +59,8 @@ ClassicEditor type: 'pen' } ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-highlight/docs/_snippets/features/custom-highlight-options.js b/packages/ckeditor5-highlight/docs/_snippets/features/custom-highlight-options.js index 156e11f5a32..bfe13edb0e2 100644 --- a/packages/ckeditor5-highlight/docs/_snippets/features/custom-highlight-options.js +++ b/packages/ckeditor5-highlight/docs/_snippets/features/custom-highlight-options.js @@ -52,7 +52,8 @@ ClassicEditor type: 'pen' } ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-highlight/docs/_snippets/features/highlight-buttons.js b/packages/ckeditor5-highlight/docs/_snippets/features/highlight-buttons.js index 58447d11527..b90ef64953a 100644 --- a/packages/ckeditor5-highlight/docs/_snippets/features/highlight-buttons.js +++ b/packages/ckeditor5-highlight/docs/_snippets/features/highlight-buttons.js @@ -35,7 +35,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-highlight/docs/_snippets/features/highlight.js b/packages/ckeditor5-highlight/docs/_snippets/features/highlight.js index 7be4ddc9c8e..05c5559bad6 100644 --- a/packages/ckeditor5-highlight/docs/_snippets/features/highlight.js +++ b/packages/ckeditor5-highlight/docs/_snippets/features/highlight.js @@ -34,7 +34,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-highlight/docs/features/highlight.md b/packages/ckeditor5-highlight/docs/features/highlight.md index 845e164cd10..e46d5bb8e4b 100644 --- a/packages/ckeditor5-highlight/docs/features/highlight.md +++ b/packages/ckeditor5-highlight/docs/features/highlight.md @@ -27,15 +27,19 @@ Select the text you want to highlight. Then use the highlight toolbar button {@i Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, Highlight } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Highlight, /* ... */ ], toolbar: [ 'highlight', /* ... */ ] + highlight: { + // Configuration. + } } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -58,6 +62,7 @@ For example, the following editor supports two styles (a green marker and a red ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... highlight: { options: [ { @@ -75,10 +80,7 @@ ClassicEditor type: 'pen' } ] - }, - toolbar: [ - 'heading', '|', 'bulletedList', 'numberedList', 'highlight', 'undo', 'redo' - ] + } } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -93,6 +95,7 @@ Instead of using the (default) `'highlight'` button, the feature also supports a ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... toolbar: { items: [ 'heading', @@ -142,6 +145,7 @@ You can use inline color values in the `rgba(R, G, B, A)`, `#RRGGBB[AA]`, or `hs ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... highlight: { options: [ { @@ -166,10 +170,7 @@ ClassicEditor type: 'pen' } ] - }, - toolbar: [ - 'heading', '|', 'bulletedList', 'numberedList', 'highlight', 'undo', 'redo' - ] + } } ) .then( /* ... */ ) .catch( /* ... */ ); diff --git a/packages/ckeditor5-horizontal-line/docs/_snippets/features/horizontal-line.js b/packages/ckeditor5-horizontal-line/docs/_snippets/features/horizontal-line.js index f960d95ba5a..1e4fa82c67c 100644 --- a/packages/ckeditor5-horizontal-line/docs/_snippets/features/horizontal-line.js +++ b/packages/ckeditor5-horizontal-line/docs/_snippets/features/horizontal-line.js @@ -62,7 +62,8 @@ ClassicEditor table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-horizontal-line/docs/features/horizontal-line.md b/packages/ckeditor5-horizontal-line/docs/features/horizontal-line.md index ae56ad2166c..bdde95a2046 100644 --- a/packages/ckeditor5-horizontal-line/docs/features/horizontal-line.md +++ b/packages/ckeditor5-horizontal-line/docs/features/horizontal-line.md @@ -26,13 +26,14 @@ To insert a horizontal line in the demo below, use the toolbar button {@icon @ck Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, HorizontalLine } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ HorizontalLine, /* ... */ ], toolbar: [ 'horizontalLine', /* ... */ ], } ) diff --git a/packages/ckeditor5-html-embed/docs/_snippets/features/html-embed.js b/packages/ckeditor5-html-embed/docs/_snippets/features/html-embed.js index 3dfaf33fae1..fc706461b9e 100644 --- a/packages/ckeditor5-html-embed/docs/_snippets/features/html-embed.js +++ b/packages/ckeditor5-html-embed/docs/_snippets/features/html-embed.js @@ -44,7 +44,7 @@ const initialData =

Documentation

See:

@@ -109,7 +109,8 @@ ClassicEditor table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-html-embed/docs/features/html-embed.md b/packages/ckeditor5-html-embed/docs/features/html-embed.md index 7be306baceb..f5a643c8d36 100644 --- a/packages/ckeditor5-html-embed/docs/features/html-embed.md +++ b/packages/ckeditor5-html-embed/docs/features/html-embed.md @@ -48,15 +48,19 @@ We recommended using the {@link features/media-embed media embed} feature for em Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, HtmlEmbed } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ HtmlEmbed, /* ... */ ], toolbar: [ 'htmlEmbed', /* ... */ ], + htmlEmbed: { + // Configuration. + } } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -73,8 +77,7 @@ However, by showing previews of the embedded HTML snippets, you expose the users ```js ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ HtmlEmbed, /* ... */ ], - toolbar: [ 'htmlEmbed', /* ... */ ], + // ... Other configuration options ... htmlEmbed: { showPreviews: true, sanitizeHtml: ( inputHtml ) => { diff --git a/packages/ckeditor5-html-support/docs/_snippets/features/full-page-html.js b/packages/ckeditor5-html-support/docs/_snippets/features/full-page-html.js index 123239c6cc6..7741fc9d35f 100644 --- a/packages/ckeditor5-html-support/docs/_snippets/features/full-page-html.js +++ b/packages/ckeditor5-html-support/docs/_snippets/features/full-page-html.js @@ -70,7 +70,8 @@ ClassicEditor }, { name: 'script' } ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-html-support/docs/_snippets/features/general-html-support-source.js b/packages/ckeditor5-html-support/docs/_snippets/features/general-html-support-source.js index 3334ef24271..5059d93ff1e 100644 --- a/packages/ckeditor5-html-support/docs/_snippets/features/general-html-support-source.js +++ b/packages/ckeditor5-html-support/docs/_snippets/features/general-html-support-source.js @@ -60,7 +60,8 @@ ClassicEditor.defaultConfig = { tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' }; window.ClassicEditor = ClassicEditor; diff --git a/packages/ckeditor5-html-support/docs/_snippets/features/general-html-support.js b/packages/ckeditor5-html-support/docs/_snippets/features/general-html-support.js index 6249ad7c173..b690953160f 100644 --- a/packages/ckeditor5-html-support/docs/_snippets/features/general-html-support.js +++ b/packages/ckeditor5-html-support/docs/_snippets/features/general-html-support.js @@ -69,7 +69,8 @@ ClassicEditor }, { name: 'script' } ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-html-support/docs/_snippets/features/html-comment.js b/packages/ckeditor5-html-support/docs/_snippets/features/html-comment.js index 8b4a8c5bb0e..81ab74f96a7 100644 --- a/packages/ckeditor5-html-support/docs/_snippets/features/html-comment.js +++ b/packages/ckeditor5-html-support/docs/_snippets/features/html-comment.js @@ -33,7 +33,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor2 = editor; diff --git a/packages/ckeditor5-html-support/docs/features/full-page-html.md b/packages/ckeditor5-html-support/docs/features/full-page-html.md index c898e24713d..11695c68b0c 100644 --- a/packages/ckeditor5-html-support/docs/features/full-page-html.md +++ b/packages/ckeditor5-html-support/docs/features/full-page-html.md @@ -25,13 +25,14 @@ Use the {@link features/source-editing source editing feature} toolbar button {@ Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, FullPage } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ FullPage, /* ... */ ], } ) .then( /* ... */ ) diff --git a/packages/ckeditor5-html-support/docs/features/general-html-support.md b/packages/ckeditor5-html-support/docs/features/general-html-support.md index a0aeb0daa42..419b4fd29bc 100644 --- a/packages/ckeditor5-html-support/docs/features/general-html-support.md +++ b/packages/ckeditor5-html-support/docs/features/general-html-support.md @@ -61,14 +61,18 @@ Therefore, the main use cases for GHS would be: Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, GeneralHtmlSupport } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ GeneralHtmlSupport, /* ... */ ], + htmlSupport: { + // Configuration. + } } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -81,6 +85,7 @@ By default, enabling the {@link module:html-support/generalhtmlsupport~GeneralHt ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... htmlSupport: { allow: [ /* HTML features to allow. */ ], disallow: [ /* HTML features to disallow. */ ] diff --git a/packages/ckeditor5-html-support/docs/features/html-comments.md b/packages/ckeditor5-html-support/docs/features/html-comments.md index 163d8484534..70f17b7c5a1 100644 --- a/packages/ckeditor5-html-support/docs/features/html-comments.md +++ b/packages/ckeditor5-html-support/docs/features/html-comments.md @@ -31,21 +31,14 @@ The editor below is configured to keep HTML comments in the document content. Yo Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -To add this feature to your rich-text editor, install the [`@ckeditor/ckeditor5-html-support`](https://www.npmjs.com/package/@ckeditor/ckeditor5-html-support) package: - -This package is part of our open-source aggregate package. - -```bash -npm install ckeditor5 -``` - -Then add it to the editor configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { HtmlComment } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ HtmlComment, ... ], } ) .then( ... ) diff --git a/packages/ckeditor5-image/docs/_snippets/features/image-caption.js b/packages/ckeditor5-image/docs/_snippets/features/image-caption.js index 02a3871488b..d4b382388a7 100644 --- a/packages/ckeditor5-image/docs/_snippets/features/image-caption.js +++ b/packages/ckeditor5-image/docs/_snippets/features/image-caption.js @@ -43,7 +43,8 @@ ClassicEditor allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editorCaption = editor; diff --git a/packages/ckeditor5-image/docs/_snippets/features/image-full.js b/packages/ckeditor5-image/docs/_snippets/features/image-full.js index 6c5f0dfb664..facb8a21d7b 100644 --- a/packages/ckeditor5-image/docs/_snippets/features/image-full.js +++ b/packages/ckeditor5-image/docs/_snippets/features/image-full.js @@ -76,7 +76,8 @@ ClassicEditor 'mergeTableCells' ] }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-image/docs/_snippets/features/image-insert-via-pasting-url-into-editor.js b/packages/ckeditor5-image/docs/_snippets/features/image-insert-via-pasting-url-into-editor.js index 71b22a8b910..d43a5d7f5a6 100644 --- a/packages/ckeditor5-image/docs/_snippets/features/image-insert-via-pasting-url-into-editor.js +++ b/packages/ckeditor5-image/docs/_snippets/features/image-insert-via-pasting-url-into-editor.js @@ -39,7 +39,8 @@ ClassicEditor '|', 'ckboxImageEdit' ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editorInsertImageViaPastingUrlIntoEditor = editor; diff --git a/packages/ckeditor5-image/docs/_snippets/features/image-insert-via-url.js b/packages/ckeditor5-image/docs/_snippets/features/image-insert-via-url.js index ca1f07031ed..2f461b63d8f 100644 --- a/packages/ckeditor5-image/docs/_snippets/features/image-insert-via-url.js +++ b/packages/ckeditor5-image/docs/_snippets/features/image-insert-via-url.js @@ -46,7 +46,8 @@ ClassicEditor integrations: [ 'url' ] } }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editorInsertImageViaUrl = editor; diff --git a/packages/ckeditor5-image/docs/_snippets/features/image-link.js b/packages/ckeditor5-image/docs/_snippets/features/image-link.js index 7b00df4b1b7..c6d10e7a010 100644 --- a/packages/ckeditor5-image/docs/_snippets/features/image-link.js +++ b/packages/ckeditor5-image/docs/_snippets/features/image-link.js @@ -41,7 +41,8 @@ ClassicEditor 'ckboxImageEdit' ] }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editorLinks = editor; diff --git a/packages/ckeditor5-image/docs/_snippets/features/image-resize-buttons-dropdown.js b/packages/ckeditor5-image/docs/_snippets/features/image-resize-buttons-dropdown.js index e51bd158a65..8bfb55a88ae 100644 --- a/packages/ckeditor5-image/docs/_snippets/features/image-resize-buttons-dropdown.js +++ b/packages/ckeditor5-image/docs/_snippets/features/image-resize-buttons-dropdown.js @@ -56,7 +56,8 @@ ClassicEditor ], toolbar: [ 'resizeImage', '|', 'ckboxImageEdit' ] }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editorResizeUIDropdown = editor; diff --git a/packages/ckeditor5-image/docs/_snippets/features/image-resize-buttons.js b/packages/ckeditor5-image/docs/_snippets/features/image-resize-buttons.js index 98051c38d61..1459c5bad4e 100644 --- a/packages/ckeditor5-image/docs/_snippets/features/image-resize-buttons.js +++ b/packages/ckeditor5-image/docs/_snippets/features/image-resize-buttons.js @@ -66,7 +66,8 @@ ClassicEditor 'ckboxImageEdit' ] }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editorResizeUI = editor; diff --git a/packages/ckeditor5-image/docs/_snippets/features/image-resize-px.js b/packages/ckeditor5-image/docs/_snippets/features/image-resize-px.js index 0e02fd70bbb..316c022f7c1 100644 --- a/packages/ckeditor5-image/docs/_snippets/features/image-resize-px.js +++ b/packages/ckeditor5-image/docs/_snippets/features/image-resize-px.js @@ -56,7 +56,8 @@ ClassicEditor allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editorResizePX = editor; diff --git a/packages/ckeditor5-image/docs/_snippets/features/image-resize.js b/packages/ckeditor5-image/docs/_snippets/features/image-resize.js index 1ed5c9cf900..f6cb472cd87 100644 --- a/packages/ckeditor5-image/docs/_snippets/features/image-resize.js +++ b/packages/ckeditor5-image/docs/_snippets/features/image-resize.js @@ -39,7 +39,8 @@ ClassicEditor 'ckboxImageEdit' ] }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editorResize = editor; diff --git a/packages/ckeditor5-image/docs/_snippets/features/image-responsive.js b/packages/ckeditor5-image/docs/_snippets/features/image-responsive.js index 714bc4d8c0d..931894edee4 100644 --- a/packages/ckeditor5-image/docs/_snippets/features/image-responsive.js +++ b/packages/ckeditor5-image/docs/_snippets/features/image-responsive.js @@ -76,7 +76,8 @@ ClassicEditor forceDemoLabel: true, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ] }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-image/docs/_snippets/features/image-semantical-style.js b/packages/ckeditor5-image/docs/_snippets/features/image-semantical-style.js index ddbf35c48a6..cfb38e25dfc 100644 --- a/packages/ckeditor5-image/docs/_snippets/features/image-semantical-style.js +++ b/packages/ckeditor5-image/docs/_snippets/features/image-semantical-style.js @@ -33,7 +33,8 @@ ClassicEditor }, image: { toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:wrapText', '|', 'ckboxImageEdit' ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editorStyleSemantical = editor; diff --git a/packages/ckeditor5-image/docs/_snippets/features/image-style-custom.js b/packages/ckeditor5-image/docs/_snippets/features/image-style-custom.js index b386212acc8..0e7644e6b9e 100644 --- a/packages/ckeditor5-image/docs/_snippets/features/image-style-custom.js +++ b/packages/ckeditor5-image/docs/_snippets/features/image-style-custom.js @@ -79,7 +79,8 @@ ClassicEditor defaultItem: 'imageStyle:block' }, '|', 'toggleImageCaption', 'linkImage', '|', 'ckboxImageEdit' ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editorStyleCustom = editor; diff --git a/packages/ckeditor5-image/docs/_snippets/features/image-style-presentational.js b/packages/ckeditor5-image/docs/_snippets/features/image-style-presentational.js index 3c1ca8208b1..01f5c37f4b4 100644 --- a/packages/ckeditor5-image/docs/_snippets/features/image-style-presentational.js +++ b/packages/ckeditor5-image/docs/_snippets/features/image-style-presentational.js @@ -63,7 +63,8 @@ ClassicEditor allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editorStylePresentational = editor; diff --git a/packages/ckeditor5-image/docs/_snippets/features/image-text-alternative.js b/packages/ckeditor5-image/docs/_snippets/features/image-text-alternative.js index 63e746c82bd..2c4e272a6bb 100644 --- a/packages/ckeditor5-image/docs/_snippets/features/image-text-alternative.js +++ b/packages/ckeditor5-image/docs/_snippets/features/image-text-alternative.js @@ -43,7 +43,8 @@ ClassicEditor allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editorCaption = editor; diff --git a/packages/ckeditor5-image/docs/features/images-captions.md b/packages/ckeditor5-image/docs/features/images-captions.md index 8ded26813f2..6f05d9415aa 100644 --- a/packages/ckeditor5-image/docs/features/images-captions.md +++ b/packages/ckeditor5-image/docs/features/images-captions.md @@ -40,12 +40,6 @@ By default, if the image caption is empty, the `
` element is not vis ## Installation - - ⚠️ **New import paths** - - Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. - - To enable this feature, you need to load the {@link module:link/linkimage~LinkImage} plugin. Read more in the {@link features/images-installation installation guide}. ## Common API diff --git a/packages/ckeditor5-image/docs/features/images-inserting.md b/packages/ckeditor5-image/docs/features/images-inserting.md index 542e8a57828..8b94a2be8d7 100644 --- a/packages/ckeditor5-image/docs/features/images-inserting.md +++ b/packages/ckeditor5-image/docs/features/images-inserting.md @@ -38,6 +38,7 @@ import { ClassicEditor, Image, ImageInsert } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Image, ImageInsert, /* ... */ ], toolbar: [ 'insertImage', /* ... */ ] } ) @@ -68,10 +69,9 @@ You can paste an image URL directly into the editor content, and it will be auto The {@link module:image/autoimage~AutoImage} plugin recognizes image links in the pasted content and embeds them shortly after they are injected into the document to speed up the editing. Accepted image extensions are: `jpg`, `jpeg`, `png`, `gif`, and `ico`. Use the following code to enable the plugin in your editor. There is no toolbar configuration for this feature. ```js -import { ClassicEditor, Image, AutoImage } from 'ckeditor5'; - ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... plugins: [ /* ... */ , Image, AutoImage ] } ) .then( /* ... */ ) diff --git a/packages/ckeditor5-image/docs/features/images-installation.md b/packages/ckeditor5-image/docs/features/images-installation.md index d38ba9e616d..5a79eed9cac 100644 --- a/packages/ckeditor5-image/docs/features/images-installation.md +++ b/packages/ckeditor5-image/docs/features/images-installation.md @@ -15,7 +15,7 @@ modified_at: 2021-06-17 Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the {@link features/images-overview#image-features subfeatures that you need} to your plugin list and to the editor toolbar: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the {@link features/images-overview#image-features subfeatures that you need} to your plugin list and to the editor toolbar: ```js import { @@ -30,8 +30,12 @@ import { ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Image, ImageToolbar, ImageCaption, ImageStyle, ImageResize, LinkImage ], toolbar: [ 'insertImage', /* ... */ ], + image: { + // Configuration. + } } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -42,11 +46,9 @@ ClassicEditor The Image feature comes with the unified image insert dropdown component {@icon @ckeditor/ckeditor5-core/theme/icons/image-upload.svg Image insert}. It automatically collects installed image insert methods. For example, if you install the `ImageUpload` plugin, the corresponding button will automatically appear in the dropdown. You only need to add a button to the toolbar: ```js -import { ClassicEditor, Image } from 'ckeditor5'; - ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ Image ], + // ... Other configuration options ... toolbar: [ 'insertImage', /* ... */ ] } ) .then( /* ... */ ) @@ -64,12 +66,9 @@ Note that the insert methods mentioned above will only be added if you install d If you need to limit the methods included in the dropdown (apart from not installing a specific feature) or change their order you can use the `image.insert.integration` configuration option: ```js -import { ClassicEditor, Image } from 'ckeditor5'; - ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ Image ], - toolbar: [ 'insertImage', /* ... */ ], + // ... Other configuration options ... image: { insert: { // This is the default configuration, you do not need to provide @@ -99,6 +98,7 @@ import { ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... plugins: [ Image, ImageToolbar, ImageCaption, ImageStyle, ImageResize, LinkImage ], toolbar: [ 'insertImage', /* ... */ ], image: { diff --git a/packages/ckeditor5-image/docs/features/images-resizing.md b/packages/ckeditor5-image/docs/features/images-resizing.md index fbdf63c6b40..320dabf47d4 100644 --- a/packages/ckeditor5-image/docs/features/images-resizing.md +++ b/packages/ckeditor5-image/docs/features/images-resizing.md @@ -45,9 +45,11 @@ import { ClassicEditor, Image, ImageResizeEditing, ImageResizeHandles } from 'ck ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Image, ImageResizeEditing, ImageResizeHandles, /* ... */ ], - // More of editor's configuration. - // ... + image: { + // Configuration. + } } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -327,9 +329,8 @@ import { ClassicEditor, Image, ImageResize } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ Image, ImageResize, /* ... */ ], - // More of editor's configuration. - // ... + // ... Other configuration options ... + plugins: [ Image, ImageResize, /* ... */ ] } ) .then( /* ... */ ) .catch( /* ... */ ); diff --git a/packages/ckeditor5-image/docs/features/images-styles.md b/packages/ckeditor5-image/docs/features/images-styles.md index aafc999ffd2..9db43bb4847 100644 --- a/packages/ckeditor5-image/docs/features/images-styles.md +++ b/packages/ckeditor5-image/docs/features/images-styles.md @@ -184,8 +184,7 @@ This editor uses custom image styles, custom image toolbar configuration with {@ ```js ClassicEditor .create( document.querySelector( '#editor' ), { - // More of editor's configuration. - // ... + // ... Other configuration options ... image: { styles: { // Defining custom styling options for the images. diff --git a/packages/ckeditor5-image/docs/features/images-text-alternative.md b/packages/ckeditor5-image/docs/features/images-text-alternative.md index b2b78d80f0c..7dc0ef06de5 100644 --- a/packages/ckeditor5-image/docs/features/images-text-alternative.md +++ b/packages/ckeditor5-image/docs/features/images-text-alternative.md @@ -48,12 +48,6 @@ When using the {@link features/ckbox CKBox file manager}, you can utilize its {@ ## Installation - - ⚠️ **New import paths** - - Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. - - Check out the {@link features/images-installation image features installation guide} to learn how to enable this feature. ## Common API diff --git a/packages/ckeditor5-image/docs/framework/deep-dive/upload-adapter.md b/packages/ckeditor5-image/docs/framework/deep-dive/upload-adapter.md index 4931e455f01..1cf90005cb4 100644 --- a/packages/ckeditor5-image/docs/framework/deep-dive/upload-adapter.md +++ b/packages/ckeditor5-image/docs/framework/deep-dive/upload-adapter.md @@ -354,10 +354,6 @@ Other image sizes can also be provided in the response, allowing [responsive ima The {@link module:image/imageupload~ImageUpload image upload} plugin is capable of handling multiple image sizes returned by the upload adapter. It will automatically add the URLs to other images sizes to the `srcset` attribute of the image in the content. - - The {@link features/easy-image Easy Image} feature provides responsive image support {@link features/easy-image#responsive-images out of the box}. - - Knowing that, you can implement the `XMLHttpRequest#load` listener that resolves the upload promise in the [previous section](#using-xmlhttprequest-in-an-adapter) so that it passes the entire `urls` property of the server response to the image upload plugin: ```js diff --git a/packages/ckeditor5-indent/docs/_snippets/features/custom-indent-block-classes.js b/packages/ckeditor5-indent/docs/_snippets/features/custom-indent-block-classes.js index 1689e6aed96..8a018021c07 100644 --- a/packages/ckeditor5-indent/docs/_snippets/features/custom-indent-block-classes.js +++ b/packages/ckeditor5-indent/docs/_snippets/features/custom-indent-block-classes.js @@ -41,7 +41,8 @@ ClassicEditor 'custom-block-indent-b', 'custom-block-indent-c' ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-indent/docs/_snippets/features/indent.js b/packages/ckeditor5-indent/docs/_snippets/features/indent.js index c294621c63d..ba9d26fbb02 100644 --- a/packages/ckeditor5-indent/docs/_snippets/features/indent.js +++ b/packages/ckeditor5-indent/docs/_snippets/features/indent.js @@ -34,7 +34,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-indent/docs/features/indent.md b/packages/ckeditor5-indent/docs/features/indent.md index 6798d569c18..62c44d660bd 100644 --- a/packages/ckeditor5-indent/docs/features/indent.md +++ b/packages/ckeditor5-indent/docs/features/indent.md @@ -26,15 +26,19 @@ Use the indent {@icon @ckeditor/ckeditor5-core/theme/icons/indent.svg Indent} or Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, Indent, IndentBlock } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Indent, IndentBlock, /* ... */ ], toolbar: [ 'outdent', 'indent', /* ... */ ] + indentBlock: { + // Configuration. + } } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -53,14 +57,9 @@ The rich-text editor from the {@link features/indent#demo demo} section above us You can change that value to, for example, `1em`: ```js -import { ClassicEditor, Indent } from 'ckeditor5'; - ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ Indent, /* ... */ ], - toolbar: { - items: [ 'heading', '|', 'outdent', 'indent', '|', 'bulletedList', 'numberedList', '|', 'undo', 'redo' ] - }, + // ... Other configuration options ... indentBlock: { offset: 1, unit: 'em' @@ -77,14 +76,9 @@ If you want more semantics in your content, use CSS classes instead of fixed ind Here is how you can configure the block indentation feature to set indentation by applying one of the defined CSS classes: ```js -import { ClassicEditor, Indent, IndentBlock } from 'ckeditor5'; - ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ Indent, IndentBlock, /* ... */ ], - toolbar: { - items: [ 'heading', '|', 'outdent', 'indent', '|', 'bulletedList', 'numberedList', '|', 'undo', 'redo' ] - }, + // ... Other configuration options ... indentBlock: { classes: [ 'custom-block-indent-a', // First step - smallest indentation. diff --git a/packages/ckeditor5-language/docs/_snippets/features/textpartlanguage.js b/packages/ckeditor5-language/docs/_snippets/features/textpartlanguage.js index 1b75eb210a3..fad93583e7a 100644 --- a/packages/ckeditor5-language/docs/_snippets/features/textpartlanguage.js +++ b/packages/ckeditor5-language/docs/_snippets/features/textpartlanguage.js @@ -55,7 +55,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-language/docs/features/language.md b/packages/ckeditor5-language/docs/features/language.md index 6c0d9dd4930..5464dafeab0 100644 --- a/packages/ckeditor5-language/docs/features/language.md +++ b/packages/ckeditor5-language/docs/features/language.md @@ -32,15 +32,19 @@ The text part language feature implements the [WCAG 3.1.2 Language of Parts](htt Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, TextPartLanguage } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ TextPartLanguage, /* ... */ ], toolbar: [ 'textPartLanguage', /* ... */ ] + language: { + // Configuration. + } } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -55,8 +59,7 @@ The example below shows the configuration used for the [demo](#demo) above: ```js ClassicEditor .create( document.querySelector( '#editor' ), { - // More of editor's configuration. - // ... + // ... Other configuration options ... language: { textPartLanguage: [ { title: 'Arabic', languageCode: 'ar' }, diff --git a/packages/ckeditor5-link/docs/_snippets/features/autolink.js b/packages/ckeditor5-link/docs/_snippets/features/autolink.js index 3ccd768951a..cdf0d6725c0 100644 --- a/packages/ckeditor5-link/docs/_snippets/features/autolink.js +++ b/packages/ckeditor5-link/docs/_snippets/features/autolink.js @@ -29,7 +29,8 @@ ClassicEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-link/docs/_snippets/features/link.js b/packages/ckeditor5-link/docs/_snippets/features/link.js index 1adfcbcce43..a44deb14b60 100644 --- a/packages/ckeditor5-link/docs/_snippets/features/link.js +++ b/packages/ckeditor5-link/docs/_snippets/features/link.js @@ -34,7 +34,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-link/docs/_snippets/features/linkdecorators.js b/packages/ckeditor5-link/docs/_snippets/features/linkdecorators.js index f78180b34e2..99232be587e 100644 --- a/packages/ckeditor5-link/docs/_snippets/features/linkdecorators.js +++ b/packages/ckeditor5-link/docs/_snippets/features/linkdecorators.js @@ -45,7 +45,8 @@ ClassicEditor } } ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { if ( !window.editors ) { diff --git a/packages/ckeditor5-link/docs/features/link.md b/packages/ckeditor5-link/docs/features/link.md index 283a5eddd78..32448226912 100644 --- a/packages/ckeditor5-link/docs/features/link.md +++ b/packages/ckeditor5-link/docs/features/link.md @@ -38,15 +38,19 @@ CKEditor 5 allows for typing both at the inner and outer boundaries of link Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, AutoLink, Link } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Link, AutoLink, /* ... */ ], toolbar: [ 'link', /* ... */ ], + link: { + // Configuration. + } } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -76,13 +80,7 @@ The following code runs this editor. Learn more about the [configuration](#confi ```js ClassicEditor .create( document.querySelector( '#editor' ), { - toolbar: { - items: [ - 'link', - // More toolbar items. - // ... - ], - }, + // ... Other configuration options ... link: { // Automatically add target="_blank" and rel="noopener noreferrer" to all external links. addTargetToExternalLinks: true, @@ -120,6 +118,7 @@ A common use case for (automatic) link decorators is adding the `target="_blank" ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... link: { addTargetToExternalLinks: true } @@ -135,6 +134,7 @@ Internally, this configuration corresponds to an [automatic decorator](#adding-a ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... link: { decorators: { addTargetToExternalLinks: { @@ -159,6 +159,7 @@ If you want to leave the decision whether a link should open in a new tab to the ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... link: { decorators: { openInNewTab: { @@ -189,6 +190,7 @@ See a basic configuration example: ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... link: { defaultProtocol: 'http://' } @@ -214,6 +216,7 @@ See a configuration example: ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... link: { // You can use `s?` suffix like below to allow both `http` and `https` protocols at the same time. allowedProtocols: [ 'https?', 'tel', 'sms', 'sftp', 'smb', 'slack' ] @@ -238,6 +241,7 @@ For instance, to create an automatic decorator that adds the `download="file.pdf ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... link: { decorators: { detectDownloadable: { @@ -269,6 +273,7 @@ To configure a "Downloadable" switch button in the link editing balloon that add ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... link: { decorators: { toggleDownloadable: { @@ -289,8 +294,6 @@ ClassicEditor } } } - // More of the editor's configuration. - // ... } ) .then( /* ... */ ) .catch( /* ... */ ); diff --git a/packages/ckeditor5-list/docs/_snippets/features/lists-basic.js b/packages/ckeditor5-list/docs/_snippets/features/lists-basic.js index fcaa4dcc5c1..92f0980aeac 100644 --- a/packages/ckeditor5-list/docs/_snippets/features/lists-basic.js +++ b/packages/ckeditor5-list/docs/_snippets/features/lists-basic.js @@ -51,7 +51,8 @@ ClassicEditor reversed: true } }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editorBasic = editor; diff --git a/packages/ckeditor5-list/docs/_snippets/features/lists-document.js b/packages/ckeditor5-list/docs/_snippets/features/lists-document.js index 5e810691415..b9070dd03c0 100644 --- a/packages/ckeditor5-list/docs/_snippets/features/lists-document.js +++ b/packages/ckeditor5-list/docs/_snippets/features/lists-document.js @@ -51,7 +51,8 @@ ClassicEditor reversed: true } }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editorStyles = editor; diff --git a/packages/ckeditor5-list/docs/_snippets/features/lists-index.js b/packages/ckeditor5-list/docs/_snippets/features/lists-index.js index bd2dd674cd2..be890361566 100644 --- a/packages/ckeditor5-list/docs/_snippets/features/lists-index.js +++ b/packages/ckeditor5-list/docs/_snippets/features/lists-index.js @@ -51,7 +51,8 @@ ClassicEditor reversed: false } }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editorStyles = editor; diff --git a/packages/ckeditor5-list/docs/_snippets/features/lists-reversed.js b/packages/ckeditor5-list/docs/_snippets/features/lists-reversed.js index caf4a0f4f30..d2b3194451a 100644 --- a/packages/ckeditor5-list/docs/_snippets/features/lists-reversed.js +++ b/packages/ckeditor5-list/docs/_snippets/features/lists-reversed.js @@ -51,7 +51,8 @@ ClassicEditor reversed: true } }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editorStyles = editor; diff --git a/packages/ckeditor5-list/docs/_snippets/features/lists-style.js b/packages/ckeditor5-list/docs/_snippets/features/lists-style.js index 343b6f4dfc6..475fb839b28 100644 --- a/packages/ckeditor5-list/docs/_snippets/features/lists-style.js +++ b/packages/ckeditor5-list/docs/_snippets/features/lists-style.js @@ -51,7 +51,8 @@ ClassicEditor reversed: false } }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editorStyles = editor; diff --git a/packages/ckeditor5-list/docs/_snippets/features/todo-list.js b/packages/ckeditor5-list/docs/_snippets/features/todo-list.js index aec38b06b2f..48df56b60ba 100644 --- a/packages/ckeditor5-list/docs/_snippets/features/todo-list.js +++ b/packages/ckeditor5-list/docs/_snippets/features/todo-list.js @@ -44,7 +44,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-list/docs/features/lists-editing.md b/packages/ckeditor5-list/docs/features/lists-editing.md index af7deb89ba3..f86ae1651f5 100644 --- a/packages/ckeditor5-list/docs/features/lists-editing.md +++ b/packages/ckeditor5-list/docs/features/lists-editing.md @@ -33,6 +33,7 @@ import { ClassicEditor, List } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ List, /* ... */ ], toolbar: [ 'bulletedList', 'numberedList', /* ... */ ], list: { @@ -56,6 +57,7 @@ import { ClassicEditor, List, AdjacentListsSupport } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ List, AdjacentListsSupport, /* ... */ ] } ) .then( /* ... */ ) diff --git a/packages/ckeditor5-list/docs/features/lists.md b/packages/ckeditor5-list/docs/features/lists.md index 309f51e51bc..0a8d5028e97 100644 --- a/packages/ckeditor5-list/docs/features/lists.md +++ b/packages/ckeditor5-list/docs/features/lists.md @@ -84,15 +84,19 @@ The `List` plugin provides the {@link features/lists ordered (numbered) and unor ### List feature -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js -import { List } from 'ckeditor5'; +import { ClassicEditor, List } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ List, /* ... */ ], toolbar: [ 'bulletedList', 'numberedList', /* ... */ ] + list: { + // Configuration. + } } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -100,16 +104,16 @@ ClassicEditor ### List properties -After {@link getting-started/quick-start installing the editor}, add `ListProperties` to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add `ListProperties` to your plugin list and toolbar configuration: To enable selected sub-features of the list properties, add their configuration to your editor. Set `true` for each feature you want to enable: ```js -import { ListProperties } from 'ckeditor5'; +import { ClassicEditor, List, ListProperties } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ ListProperties, /* ... */ ], + plugins: [ List, ListProperties, /* ... */ ], toolbar: [ 'bulletedList', 'numberedList', /* ... */ ], list: { properties: { diff --git a/packages/ckeditor5-list/docs/features/todo-lists.md b/packages/ckeditor5-list/docs/features/todo-lists.md index e2066114269..69b1965e792 100644 --- a/packages/ckeditor5-list/docs/features/todo-lists.md +++ b/packages/ckeditor5-list/docs/features/todo-lists.md @@ -31,15 +31,19 @@ You can check and clear a list item by using the Ctrl + Enter -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js -import { TodoList } from 'ckeditor5'; +import { ClassicEditor, TodoList } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ TodoList, /* ... */ ], toolbar: [ 'todoList', /* ... */ ], + list: { + // Configuration. + } } ) .then( /* ... */ ) .catch( /* ... */ ); diff --git a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.js b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.js index da83b068f16..5cb1b462b92 100644 --- a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.js +++ b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/markdown.js @@ -67,7 +67,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.js b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.js index 15634d12033..c63e481d8b1 100644 --- a/packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.js +++ b/packages/ckeditor5-markdown-gfm/docs/_snippets/features/paste-from-markdown.js @@ -80,7 +80,8 @@ ClassicEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-markdown-gfm/docs/features/markdown.md b/packages/ckeditor5-markdown-gfm/docs/features/markdown.md index e116ddbd69c..9ce192f14e4 100644 --- a/packages/ckeditor5-markdown-gfm/docs/features/markdown.md +++ b/packages/ckeditor5-markdown-gfm/docs/features/markdown.md @@ -35,25 +35,21 @@ Please remember that Markdown syntax is really simple and it does not cover all Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the {@link module:markdown-gfm/markdown~Markdown} plugin to the editor configuration. It will change the default {@link module:engine/dataprocessor/dataprocessor~DataProcessor data processor} to the {@link module:markdown-gfm/gfmdataprocessor~GFMDataProcessor}: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the {@link module:markdown-gfm/markdown~Markdown} plugin to the editor configuration. It will change the default {@link module:engine/dataprocessor/dataprocessor~DataProcessor data processor} to the {@link module:markdown-gfm/gfmdataprocessor~GFMDataProcessor}: ```js import { ClassicEditor, Bold, Italic, Essentials, Markdown } from 'ckeditor5'; -// More imports. -// ... ClassicEditor .create( document.querySelector( '#snippet-markdown' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Markdown, Essentials, Bold, Italic, // More plugins. - // ... ], - // More of editor's configuration. - // ... } ) .then( /* ... */ ) .catch( /* ... */ ); diff --git a/packages/ckeditor5-markdown-gfm/docs/features/paste-markdown.md b/packages/ckeditor5-markdown-gfm/docs/features/paste-markdown.md index 1edc6469772..17740653f71 100644 --- a/packages/ckeditor5-markdown-gfm/docs/features/paste-markdown.md +++ b/packages/ckeditor5-markdown-gfm/docs/features/paste-markdown.md @@ -33,15 +33,14 @@ Paste some Markdown-formatted content into the demo editor below and see it turn Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, Bold, Italic, Essentials, PasteFromMarkdownExperimental } from 'ckeditor5'; -// More imports. -// ... ClassicEditor .create( document.querySelector( '#snippet-markdown' ), { + licenseKey: '', // Or 'GPL'. plugins: [ PasteFromMarkdownExperimental, Essentials, @@ -50,8 +49,6 @@ ClassicEditor // More plugins. // ... ], - // More of editor's configuration. - // ... } ) .then( /* ... */ ) .catch( /* ... */ ); diff --git a/packages/ckeditor5-media-embed/docs/_snippets/features/media-embed-preview.js b/packages/ckeditor5-media-embed/docs/_snippets/features/media-embed-preview.js index 9fa2d8125db..62aa6ff2b4a 100644 --- a/packages/ckeditor5-media-embed/docs/_snippets/features/media-embed-preview.js +++ b/packages/ckeditor5-media-embed/docs/_snippets/features/media-embed-preview.js @@ -60,7 +60,8 @@ ClassicEditor } } ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-media-embed/docs/_snippets/features/media-embed.js b/packages/ckeditor5-media-embed/docs/_snippets/features/media-embed.js index 29aefe156d7..a221fcb2efd 100644 --- a/packages/ckeditor5-media-embed/docs/_snippets/features/media-embed.js +++ b/packages/ckeditor5-media-embed/docs/_snippets/features/media-embed.js @@ -34,7 +34,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-media-embed/docs/features/media-embed.md b/packages/ckeditor5-media-embed/docs/features/media-embed.md index 543c6886ce7..4c72b742cec 100644 --- a/packages/ckeditor5-media-embed/docs/features/media-embed.md +++ b/packages/ckeditor5-media-embed/docs/features/media-embed.md @@ -33,18 +33,18 @@ You can use the insert media button in the toolbar {@icon @ckeditor/ckeditor5-me Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, MediaEmbed } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ MediaEmbed, /* ... */ ], toolbar: [ 'mediaEmbed', /* ... */ ] mediaEmbed: { - // Configuration - // ... + // Configuration. } } ) .then( /* ... */ ) @@ -177,8 +177,7 @@ For instance, to leave only the previewable providers, configure this feature as ```js ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ MediaEmbed, /* ... */ ], - toolbar: [ 'mediaEmbed', /* ... */ ] + // ... Other configuration options ... mediaEmbed: { removeProviders: [ 'instagram', 'twitter', 'googleMaps', 'flickr', 'facebook' ] } @@ -194,8 +193,7 @@ To override the default providers, use {@link module:media-embed/mediaembedconfi ```js ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ MediaEmbed, /* ... */ ],, - toolbar: [ 'mediaEmbed', /* ... */ ] + // ... Other configuration options ... mediaEmbed: { providers: [ { diff --git a/packages/ckeditor5-mention/docs/_snippets/examples/chat-with-mentions.js b/packages/ckeditor5-mention/docs/_snippets/examples/chat-with-mentions.js index e65950d54a2..e8c89bbb6a9 100644 --- a/packages/ckeditor5-mention/docs/_snippets/examples/chat-with-mentions.js +++ b/packages/ckeditor5-mention/docs/_snippets/examples/chat-with-mentions.js @@ -73,7 +73,8 @@ ClassicEditor ] } ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { const editingView = editor.editing.view; diff --git a/packages/ckeditor5-mention/docs/_snippets/features/custom-mention-colors-variables.js b/packages/ckeditor5-mention/docs/_snippets/features/custom-mention-colors-variables.js index c2d09d721e1..41268ab0e82 100644 --- a/packages/ckeditor5-mention/docs/_snippets/features/custom-mention-colors-variables.js +++ b/packages/ckeditor5-mention/docs/_snippets/features/custom-mention-colors-variables.js @@ -42,7 +42,8 @@ ClassicEditor feed: [ '@Barney', '@Lily', '@Marry Ann', '@Marshall', '@Robin', '@Ted' ] } ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-mention/docs/_snippets/features/mention-customization.js b/packages/ckeditor5-mention/docs/_snippets/features/mention-customization.js index 79909a2bc53..98875df69d2 100644 --- a/packages/ckeditor5-mention/docs/_snippets/features/mention-customization.js +++ b/packages/ckeditor5-mention/docs/_snippets/features/mention-customization.js @@ -45,7 +45,8 @@ ClassicEditor itemRenderer: customItemRenderer } ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-mention/docs/_snippets/features/mention.js b/packages/ckeditor5-mention/docs/_snippets/features/mention.js index f0582c81654..f35c5cc1de6 100644 --- a/packages/ckeditor5-mention/docs/_snippets/features/mention.js +++ b/packages/ckeditor5-mention/docs/_snippets/features/mention.js @@ -43,7 +43,8 @@ ClassicEditor minimumCharacters: 0 } ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-mention/docs/examples/chat-with-mentions.md b/packages/ckeditor5-mention/docs/examples/chat-with-mentions.md index 00ff4f2f9bd..6a9a9d4c09b 100644 --- a/packages/ckeditor5-mention/docs/examples/chat-with-mentions.md +++ b/packages/ckeditor5-mention/docs/examples/chat-with-mentions.md @@ -36,6 +36,7 @@ import { ClassicEditor .create( document.querySelector( '.chat__editor' ), { + licenseKey: 'GPL', // Or ''. extraPlugins: [ Essentials, Paragraph, Mention, MentionLinks, Bold, Italic, Underline, Strikethrough, Link ], toolbar: { items: [ diff --git a/packages/ckeditor5-mention/docs/features/mentions.md b/packages/ckeditor5-mention/docs/features/mentions.md index 86ff78ba49a..1019b9497df 100644 --- a/packages/ckeditor5-mention/docs/features/mentions.md +++ b/packages/ckeditor5-mention/docs/features/mentions.md @@ -33,17 +33,17 @@ You can read more about possible implementations of the mention feature in a [de Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, Mention } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Mention, /* ... */ ], mention: { // Configuration. - // ... } } ) .then( /* ... */ ) @@ -59,10 +59,7 @@ The code snippet below was used to configure the demo above. It defines the list ```js ClassicEditor .create( document.querySelector( '#editor' ), { - // This feature is available in the superbuild only. - // See the "Installation" section. - plugins: [ Mention, /* ... */ ], - + // ... Other configuration options ... mention: { feeds: [ { @@ -103,10 +100,7 @@ The callback receives the query text which should be used to filter item suggest ```js ClassicEditor .create( document.querySelector( '#editor' ), { - // This feature is available in the superbuild only. - // See the "Installation" section. - plugins: [ Mention, /* ... */ ], - + // ... Other configuration options ... mention: { feeds: [ { @@ -171,7 +165,7 @@ This callback takes a feed item (it contains at least the `name` property) and m ```js ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ Mention, /* ... */ ], + // ... Other configuration options ... mention: { feeds: [ { @@ -212,7 +206,7 @@ The number of items displayed in the autocomplete list can be customized by defi ```js ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ Mention, /* ... */ ], + // ... Other configuration options ... mention: { // Define the custom number of visible mentions. dropdownLimit: 4 @@ -236,7 +230,7 @@ You can control the text inserted into the editor when creating a mention via th ```js ClassicEditor .create( editorElement, { - plugins: [ Mention, ... ], + // ... Other configuration options ... mention: { feeds: [ // Feed items as objects. @@ -288,6 +282,7 @@ By default, attribute elements that are next to each other and have the same val ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... plugins: [ Mention, MentionCustomization, /* ... */ ], // Add the custom mention plugin function. mention: { // Configuration. @@ -372,6 +367,7 @@ Below is an example of a customized mention feature that: ```js ClassicEditor .create( document.querySelector( '#snippet-mention-customization' ), { + // ... Other configuration options ... plugins: [ Mention, MentionCustomization, /* ... */ ], mention: { dropdownLimit: 4, diff --git a/packages/ckeditor5-minimap/docs/_snippets/features/minimap.js b/packages/ckeditor5-minimap/docs/_snippets/features/minimap.js index b79eb54ac57..5378a3b445d 100644 --- a/packages/ckeditor5-minimap/docs/_snippets/features/minimap.js +++ b/packages/ckeditor5-minimap/docs/_snippets/features/minimap.js @@ -100,7 +100,8 @@ const config = { tokeUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' }; DecoupledEditor diff --git a/packages/ckeditor5-minimap/docs/features/minimap.md b/packages/ckeditor5-minimap/docs/features/minimap.md index 3ece7efd406..3c39f6d550a 100644 --- a/packages/ckeditor5-minimap/docs/features/minimap.md +++ b/packages/ckeditor5-minimap/docs/features/minimap.md @@ -33,17 +33,17 @@ Scroll the content, and the minimap in the sidebar will show your current locati Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { DecoupledEditor, Minimap } from 'ckeditor5'; DecoupledEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Minimap, /* ... */ ], minimap: { - // Reference to the container element as shown in the configuration section of the guide - // ... + // Configuration. } } ) .then( /* ... */ ) @@ -61,11 +61,9 @@ DecoupledEditor The container element is essential for the minimap to render. You should pass the reference to the container element in {@link module:minimap/minimapconfig~MinimapConfig#container `config.minimap.container`}. Note that it must have a fixed `width` and `overflow: hidden` when the editor is created: ```js -import { DecoupledEditor, Minimap } from 'ckeditor5'; - DecoupledEditor .create( document.querySelector( '#editor' ), { - plugins: [ Minimap, /* ... */ ], + // ... Other configuration options ... minimap: { container: document.querySelector( '.minimap-container' ) } @@ -167,11 +165,9 @@ Employ the following CSS: Finally, the JavaScript to run the editor (learn how to [install](#installation) the feature): ```js -import { DecoupledEditor, Minimap } from 'ckeditor5'; - DecoupledEditor .create( document.querySelector( '#editor-content' ), { - plugins: [ Minimap, /* ... */ ], + // ... Other configuration options ... minimap: { container: document.querySelector( '.minimap-container' ), } diff --git a/packages/ckeditor5-page-break/docs/_snippets/features/page-break.js b/packages/ckeditor5-page-break/docs/_snippets/features/page-break.js index 1ddc5521c22..dfeba65b359 100644 --- a/packages/ckeditor5-page-break/docs/_snippets/features/page-break.js +++ b/packages/ckeditor5-page-break/docs/_snippets/features/page-break.js @@ -62,7 +62,8 @@ ClassicEditor table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-page-break/docs/features/page-break.md b/packages/ckeditor5-page-break/docs/features/page-break.md index 9bbee004d08..a310eb4c1a0 100644 --- a/packages/ckeditor5-page-break/docs/features/page-break.md +++ b/packages/ckeditor5-page-break/docs/features/page-break.md @@ -26,13 +26,14 @@ Use the insert page break toolbar button {@icon @ckeditor/ckeditor5-page-break/t Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, PageBreak } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ PageBreak, /* ... */ ], toolbar: [ 'pageBreak', /* ... */ ], } ) diff --git a/packages/ckeditor5-paste-from-office/docs/_snippets/features/paste-from-office.js b/packages/ckeditor5-paste-from-office/docs/_snippets/features/paste-from-office.js index 2fb08000f7a..bcc990310fa 100644 --- a/packages/ckeditor5-paste-from-office/docs/_snippets/features/paste-from-office.js +++ b/packages/ckeditor5-paste-from-office/docs/_snippets/features/paste-from-office.js @@ -78,7 +78,8 @@ ClassicEditor } }, placeholder: 'Paste the content here to test the feature.', - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-paste-from-office/docs/features/paste-from-google-docs.md b/packages/ckeditor5-paste-from-office/docs/features/paste-from-google-docs.md index c1ced241a8e..38c1e4a3417 100644 --- a/packages/ckeditor5-paste-from-office/docs/features/paste-from-google-docs.md +++ b/packages/ckeditor5-paste-from-office/docs/features/paste-from-google-docs.md @@ -47,13 +47,14 @@ This means that if you did not enable, for instance, {@link features/font font f Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, PasteFromOffice } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ PasteFromOffice, /* ... */ ] } ) .then( /* ... */ ) diff --git a/packages/ckeditor5-paste-from-office/docs/features/paste-from-office.md b/packages/ckeditor5-paste-from-office/docs/features/paste-from-office.md index 16fac793b07..69318f28386 100644 --- a/packages/ckeditor5-paste-from-office/docs/features/paste-from-office.md +++ b/packages/ckeditor5-paste-from-office/docs/features/paste-from-office.md @@ -72,13 +72,14 @@ This means that if you did not enable, for instance, {@link features/font font f Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, PasteFromOffice } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ PasteFromOffice, /* ... */ ] } ) .then( /* ... */ ) diff --git a/packages/ckeditor5-paste-from-office/tests/_utils/utils.js b/packages/ckeditor5-paste-from-office/tests/_utils/utils.js index c0383d3f2ca..8b1576adec0 100644 --- a/packages/ckeditor5-paste-from-office/tests/_utils/utils.js +++ b/packages/ckeditor5-paste-from-office/tests/_utils/utils.js @@ -81,7 +81,9 @@ export function generateTests( config ) { describe( config.type, () => { describe( config.input, () => { - const editorConfig = config.editorConfig || {}; + const editorConfig = typeof config.editorConfig == 'function' ? + config.editorConfig : + () => Promise.resolve( config.editorConfig || {} ); for ( const group of Object.keys( groups ) ) { const skip = config.skip && config.skip[ group ] || []; @@ -152,12 +154,8 @@ function generateNormalizationTests( title, fixtures, editorConfig, skip, only ) describe( title, () => { let editor; - beforeEach( () => { - return VirtualTestEditor - .create( editorConfig ) - .then( newEditor => { - editor = newEditor; - } ); + beforeEach( async () => { + editor = await VirtualTestEditor.create( await editorConfig() ); } ); afterEach( () => { @@ -207,16 +205,12 @@ function generateIntegrationTests( title, fixtures, editorConfig, skip, only ) { let element, editor; let data = {}; - before( () => { + before( async () => { element = document.createElement( 'div' ); document.body.appendChild( element ); - return ClassicTestEditor - .create( element, editorConfig ) - .then( editorInstance => { - editor = editorInstance; - } ); + editor = await ClassicTestEditor.create( element, await editorConfig() ); } ); beforeEach( () => { diff --git a/packages/ckeditor5-remove-format/docs/_snippets/features/remove-format.js b/packages/ckeditor5-remove-format/docs/_snippets/features/remove-format.js index 5709bf384bf..202c4d9591d 100644 --- a/packages/ckeditor5-remove-format/docs/_snippets/features/remove-format.js +++ b/packages/ckeditor5-remove-format/docs/_snippets/features/remove-format.js @@ -38,7 +38,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-remove-format/docs/features/remove-format.md b/packages/ckeditor5-remove-format/docs/features/remove-format.md index d282fe61d46..a131f8e7fe9 100644 --- a/packages/ckeditor5-remove-format/docs/features/remove-format.md +++ b/packages/ckeditor5-remove-format/docs/features/remove-format.md @@ -27,13 +27,14 @@ Select the content you want to clean up and press the remove format button {@ico Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, RemoveFormat } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ RemoveFormat, /* ... */ ], toolbar: [ 'removeFormat', /* ... */ ] } ) @@ -74,6 +75,7 @@ Enable the `RemoveFormatLinks` plugin in the {@link getting-started/setup/config ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... plugins: [ RemoveFormat, RemoveFormatLinks, diff --git a/packages/ckeditor5-restricted-editing/docs/_snippets/features/restricted-editing.js b/packages/ckeditor5-restricted-editing/docs/_snippets/features/restricted-editing.js index be762f35560..90bd82f4fae 100644 --- a/packages/ckeditor5-restricted-editing/docs/_snippets/features/restricted-editing.js +++ b/packages/ckeditor5-restricted-editing/docs/_snippets/features/restricted-editing.js @@ -83,7 +83,8 @@ async function startStandardEditingMode() { allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true }, - updateSourceElementOnDestroy: true + updateSourceElementOnDestroy: true, + licenseKey: 'GPL' } ); } @@ -107,7 +108,8 @@ async function startRestrictedEditingMode() { tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ] }, - updateSourceElementOnDestroy: true + updateSourceElementOnDestroy: true, + licenseKey: 'GPL' } ); } diff --git a/packages/ckeditor5-restricted-editing/docs/features/restricted-editing.md b/packages/ckeditor5-restricted-editing/docs/features/restricted-editing.md index a340532998d..36dccff3bb2 100644 --- a/packages/ckeditor5-restricted-editing/docs/features/restricted-editing.md +++ b/packages/ckeditor5-restricted-editing/docs/features/restricted-editing.md @@ -48,7 +48,7 @@ By using this feature, the users of your application will be able to create temp Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration. +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration. ### Running the standard editing mode @@ -59,6 +59,7 @@ import { ClassicEditor, StandardEditingMode } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ StandardEditingMode, /* ... */ ], toolbar: [ 'restrictedEditingException', /* ... */ ] } ) @@ -75,6 +76,7 @@ import { ClassicEditor, RestrictedEditingMode } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ RestrictedEditingMode, /* ... */ ], toolbar: [ 'restrictedEditing', /* ... */ ] } ) @@ -91,6 +93,7 @@ import { ClassicEditor, RestrictedEditingMode, Bold } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Bold, RestrictedEditingMode, /* ... */ ], toolbar: [ 'bold', '|', 'restrictedEditing', /* ... */ ], restrictedEditing: { diff --git a/packages/ckeditor5-select-all/docs/_snippets/features/select-all.js b/packages/ckeditor5-select-all/docs/_snippets/features/select-all.js index 065b6be1bcc..a6b9e682d39 100644 --- a/packages/ckeditor5-select-all/docs/_snippets/features/select-all.js +++ b/packages/ckeditor5-select-all/docs/_snippets/features/select-all.js @@ -44,7 +44,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-select-all/docs/features/select-all.md b/packages/ckeditor5-select-all/docs/features/select-all.md index 5ebec0cdc42..b20e41745ef 100644 --- a/packages/ckeditor5-select-all/docs/features/select-all.md +++ b/packages/ckeditor5-select-all/docs/features/select-all.md @@ -28,17 +28,15 @@ Press Ctrl/Cmd+A or use the toolbar button {@ic Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, SelectAll } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { - // Load the plugin. + licenseKey: '', // Or 'GPL'. plugins: [ SelectAll, /* ... */ ], - - // Display the "Select all" button in the toolbar. toolbar: [ 'selectAll', /* ... */ ], } ) .then( /* ... */ ) diff --git a/packages/ckeditor5-show-blocks/docs/_snippets/features/show-blocks.js b/packages/ckeditor5-show-blocks/docs/_snippets/features/show-blocks.js index 7eea810426f..186478ee58e 100644 --- a/packages/ckeditor5-show-blocks/docs/_snippets/features/show-blocks.js +++ b/packages/ckeditor5-show-blocks/docs/_snippets/features/show-blocks.js @@ -58,7 +58,8 @@ ClassicEditor classes: true } ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-show-blocks/docs/features/show-blocks.md b/packages/ckeditor5-show-blocks/docs/features/show-blocks.md index 2093f84316f..fa497545459 100644 --- a/packages/ckeditor5-show-blocks/docs/features/show-blocks.md +++ b/packages/ckeditor5-show-blocks/docs/features/show-blocks.md @@ -26,17 +26,15 @@ Toggle the block elements visibility with the show block {@icon @ckeditor/ckedit Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, ShowBlocks } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { - // Load the plugin. + licenseKey: '', // Or 'GPL'. plugins: [ ShowBlocks, /* ... */ ], - - // Display the "Show blocks" button in the toolbar. toolbar: [ 'showBlocks', /* ... */ ], } ) .then( /* ... */ ) diff --git a/packages/ckeditor5-source-editing/docs/_snippets/features/source-editing-imports.js b/packages/ckeditor5-source-editing/docs/_snippets/features/source-editing-imports.js index 52a25a51ea8..96179fe5975 100644 --- a/packages/ckeditor5-source-editing/docs/_snippets/features/source-editing-imports.js +++ b/packages/ckeditor5-source-editing/docs/_snippets/features/source-editing-imports.js @@ -69,7 +69,8 @@ ClassicEditor.defaultConfig = { viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' }; window.ClassicEditor = ClassicEditor; diff --git a/packages/ckeditor5-source-editing/docs/_snippets/features/source-editing-with-markdown.js b/packages/ckeditor5-source-editing/docs/_snippets/features/source-editing-with-markdown.js index e4836f7e290..1ba95d92174 100644 --- a/packages/ckeditor5-source-editing/docs/_snippets/features/source-editing-with-markdown.js +++ b/packages/ckeditor5-source-editing/docs/_snippets/features/source-editing-with-markdown.js @@ -64,7 +64,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-source-editing/docs/_snippets/features/source-editing.js b/packages/ckeditor5-source-editing/docs/_snippets/features/source-editing.js index f36de2af597..e4d74d6cb47 100644 --- a/packages/ckeditor5-source-editing/docs/_snippets/features/source-editing.js +++ b/packages/ckeditor5-source-editing/docs/_snippets/features/source-editing.js @@ -71,7 +71,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-source-editing/docs/features/source-editing.md b/packages/ckeditor5-source-editing/docs/features/source-editing.md index efc79583cba..babb1255af0 100644 --- a/packages/ckeditor5-source-editing/docs/features/source-editing.md +++ b/packages/ckeditor5-source-editing/docs/features/source-editing.md @@ -30,13 +30,14 @@ You can also use one of the many CKEditor 5 features available in the toolb Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, SourceEditing } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ SourceEditing, /* ... */ ], toolbar: [ 'sourceEditing', /* ... */ ] } ) @@ -51,6 +52,7 @@ import { ClassicEditor, SourceEditing, Markdown } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ SourceEditing, Markdown, /* ... */ ], toolbar: [ 'sourceEditing', /* ... */ ] } ) diff --git a/packages/ckeditor5-special-characters/docs/_snippets/features/special-characters-extended-category.js b/packages/ckeditor5-special-characters/docs/_snippets/features/special-characters-extended-category.js index 9560e9bb067..86183c8a5cd 100644 --- a/packages/ckeditor5-special-characters/docs/_snippets/features/special-characters-extended-category.js +++ b/packages/ckeditor5-special-characters/docs/_snippets/features/special-characters-extended-category.js @@ -52,7 +52,8 @@ ClassicEditor table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-special-characters/docs/_snippets/features/special-characters-limited-categories.js b/packages/ckeditor5-special-characters/docs/_snippets/features/special-characters-limited-categories.js index 280c8979e65..ca21511a325 100644 --- a/packages/ckeditor5-special-characters/docs/_snippets/features/special-characters-limited-categories.js +++ b/packages/ckeditor5-special-characters/docs/_snippets/features/special-characters-limited-categories.js @@ -44,7 +44,8 @@ ClassicEditor table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-special-characters/docs/_snippets/features/special-characters-new-category.js b/packages/ckeditor5-special-characters/docs/_snippets/features/special-characters-new-category.js index bbc1dba8712..95a92a4e702 100644 --- a/packages/ckeditor5-special-characters/docs/_snippets/features/special-characters-new-category.js +++ b/packages/ckeditor5-special-characters/docs/_snippets/features/special-characters-new-category.js @@ -54,7 +54,8 @@ ClassicEditor table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-special-characters/docs/_snippets/features/special-characters.js b/packages/ckeditor5-special-characters/docs/_snippets/features/special-characters.js index 1efad45622d..c18a9fd7b05 100644 --- a/packages/ckeditor5-special-characters/docs/_snippets/features/special-characters.js +++ b/packages/ckeditor5-special-characters/docs/_snippets/features/special-characters.js @@ -44,7 +44,8 @@ ClassicEditor table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-special-characters/docs/features/special-characters.md b/packages/ckeditor5-special-characters/docs/features/special-characters.md index 79aceab3211..672e2d677bd 100644 --- a/packages/ckeditor5-special-characters/docs/features/special-characters.md +++ b/packages/ckeditor5-special-characters/docs/features/special-characters.md @@ -28,7 +28,7 @@ Use the special characters toolbar button {@icon @ckeditor/ckeditor5-special-cha Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js // Core plugin provides the API for the management of special characters and their categories. @@ -37,8 +37,12 @@ import { ClassicEditor, SpecialCharacters, SpecialCharactersEssentials } from 'c ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ SpecialCharacters, SpecialCharactersEssentials, /* ... */ ], toolbar: [ 'specialCharacters', /* ... */ ], + specialCharacters: { + // Configuration. + } } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -55,8 +59,6 @@ You can define a new special characters category using the {@link module:special For example, the following plugin adds the "Emoji" category to the special characters dropdown. ```js -import { ClassicEditor, SpecialCharacters, SpecialCharactersEssentials } from 'ckeditor5'; - function SpecialCharactersEmoji( editor ) { editor.plugins.get( 'SpecialCharacters' ).addItems( 'Emoji', [ { title: 'smiley face', character: '😊' }, @@ -69,12 +71,10 @@ function SpecialCharactersEmoji( editor ) { ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... plugins: [ SpecialCharacters, SpecialCharactersEssentials, SpecialCharactersEmoji, - // More plugins. - // ... ], - toolbar: [ 'specialCharacters', /* ... */ ], } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -99,8 +99,6 @@ Below you can see a demo based on the example shown above. Use the special chara By using the {@link module:special-characters/specialcharacters~SpecialCharacters#addItems `SpecialCharacters#addItems()`} function you can also add new special characters to an existing category. ```js -import { ClassicEditor, SpecialCharacters, SpecialCharactersEssentials } from 'ckeditor5'; - function SpecialCharactersExtended( editor ) { editor.plugins.get( 'SpecialCharacters' ).addItems( 'Mathematical', [ { title: 'alpha', character: 'α' }, @@ -111,13 +109,10 @@ function SpecialCharactersExtended( editor ) { ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... plugins: [ SpecialCharacters, SpecialCharactersEssentials, SpecialCharactersExtended, - - // More plugins. - // ... ], - toolbar: [ 'specialCharacters', /* ... */ ], } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -151,13 +146,10 @@ import { ClassicEditor, SpecialCharacters, SpecialCharactersCurrency, SpecialCha ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... plugins: [ SpecialCharacters, SpecialCharactersCurrency, SpecialCharactersMathematical, - - // More plugins. - // ... ], - toolbar: [ 'specialCharacters', /* ... */ ], } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -176,6 +168,7 @@ The categories order can be customized using the {@link module:special-character ```js ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... plugins: [ SpecialCharacters, SpecialCharactersEssentials, ... ], specialCharacters: { order: [ diff --git a/packages/ckeditor5-style/docs/_snippets/features/styles.js b/packages/ckeditor5-style/docs/_snippets/features/styles.js index 4c609ca187a..5b8773aedf6 100644 --- a/packages/ckeditor5-style/docs/_snippets/features/styles.js +++ b/packages/ckeditor5-style/docs/_snippets/features/styles.js @@ -122,7 +122,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-style/docs/features/style.md b/packages/ckeditor5-style/docs/features/style.md index ab8cceac715..7037f90fb2c 100644 --- a/packages/ckeditor5-style/docs/features/style.md +++ b/packages/ckeditor5-style/docs/features/style.md @@ -231,26 +231,19 @@ The style sheet: Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, Style } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Style, /* ... */ ], - toolbar: { - items: [ - 'style', - // More toolbar items. - // ... - ], + toolbar: [ 'style', /* ... */ ], }, style: { - definitions: [ - // Styles definitions. - // ... - ] + // Configuration. } } ) .then( /* ... */ ) @@ -264,14 +257,7 @@ Configuring the styles feature takes two steps. First, you need to define the st ```js ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ Style, /* ... */ ], - toolbar: { - items: [ - 'style', - // More toolbar items. - // ... - ], - }, + // ... Other configuration options ... style: { definitions: [ { diff --git a/packages/ckeditor5-table/docs/_snippets/features/build-table-source.js b/packages/ckeditor5-table/docs/_snippets/features/build-table-source.js index 13a0002f2b0..1619e713ab3 100644 --- a/packages/ckeditor5-table/docs/_snippets/features/build-table-source.js +++ b/packages/ckeditor5-table/docs/_snippets/features/build-table-source.js @@ -44,7 +44,8 @@ ClassicEditor.defaultConfig = { top: window.getViewportTopOffsetConfig() } }, - indentBlock: { offset: 30, unit: 'px' } + indentBlock: { offset: 30, unit: 'px' }, + licenseKey: 'GPL' }; window.ClassicEditor = ClassicEditor; diff --git a/packages/ckeditor5-table/docs/_snippets/features/table-caption.js b/packages/ckeditor5-table/docs/_snippets/features/table-caption.js index 7eaa5e90466..25642a1db13 100644 --- a/packages/ckeditor5-table/docs/_snippets/features/table-caption.js +++ b/packages/ckeditor5-table/docs/_snippets/features/table-caption.js @@ -34,7 +34,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editorCaption = editor; diff --git a/packages/ckeditor5-table/docs/_snippets/features/table-column-resize.js b/packages/ckeditor5-table/docs/_snippets/features/table-column-resize.js index eb8cdecaefe..d5bf8328251 100644 --- a/packages/ckeditor5-table/docs/_snippets/features/table-column-resize.js +++ b/packages/ckeditor5-table/docs/_snippets/features/table-column-resize.js @@ -34,7 +34,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editorCaption = editor; diff --git a/packages/ckeditor5-table/docs/_snippets/features/table-default-headings.js b/packages/ckeditor5-table/docs/_snippets/features/table-default-headings.js index 0af85a6e651..56c141852c9 100644 --- a/packages/ckeditor5-table/docs/_snippets/features/table-default-headings.js +++ b/packages/ckeditor5-table/docs/_snippets/features/table-default-headings.js @@ -34,7 +34,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editorDefaultHeadings = editor; diff --git a/packages/ckeditor5-table/docs/_snippets/features/table-default-properties.js b/packages/ckeditor5-table/docs/_snippets/features/table-default-properties.js index bf99f6fbfc5..3d45dc1e338 100644 --- a/packages/ckeditor5-table/docs/_snippets/features/table-default-properties.js +++ b/packages/ckeditor5-table/docs/_snippets/features/table-default-properties.js @@ -53,7 +53,8 @@ ClassicEditor allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true }, - placeholder: 'Insert the new table with the default styles applied.' + placeholder: 'Insert the new table with the default styles applied.', + licenseKey: 'GPL' } ) .then( editor => { window.editorDefaultStyles = editor; diff --git a/packages/ckeditor5-table/docs/_snippets/features/table-nesting.js b/packages/ckeditor5-table/docs/_snippets/features/table-nesting.js index 5c595bd9ccb..ac5a6e83158 100644 --- a/packages/ckeditor5-table/docs/_snippets/features/table-nesting.js +++ b/packages/ckeditor5-table/docs/_snippets/features/table-nesting.js @@ -127,7 +127,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editorStyling = editor; diff --git a/packages/ckeditor5-table/docs/_snippets/features/table-styling-colors.js b/packages/ckeditor5-table/docs/_snippets/features/table-styling-colors.js index e09ab6ff407..99ffa3dba82 100644 --- a/packages/ckeditor5-table/docs/_snippets/features/table-styling-colors.js +++ b/packages/ckeditor5-table/docs/_snippets/features/table-styling-colors.js @@ -128,7 +128,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editorStyling = editor; diff --git a/packages/ckeditor5-table/docs/_snippets/features/table-styling.js b/packages/ckeditor5-table/docs/_snippets/features/table-styling.js index 017dabdba7e..b7602a110f0 100644 --- a/packages/ckeditor5-table/docs/_snippets/features/table-styling.js +++ b/packages/ckeditor5-table/docs/_snippets/features/table-styling.js @@ -37,7 +37,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editorStyling = editor; diff --git a/packages/ckeditor5-table/docs/_snippets/features/tables.js b/packages/ckeditor5-table/docs/_snippets/features/tables.js index 6ffbf368f39..359f1335914 100644 --- a/packages/ckeditor5-table/docs/_snippets/features/tables.js +++ b/packages/ckeditor5-table/docs/_snippets/features/tables.js @@ -40,7 +40,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-table/docs/features/tables-caption.md b/packages/ckeditor5-table/docs/features/tables-caption.md index dac34ef2936..51e86cff48d 100644 --- a/packages/ckeditor5-table/docs/features/tables-caption.md +++ b/packages/ckeditor5-table/docs/features/tables-caption.md @@ -30,13 +30,14 @@ In the demo below, click the table caption to edit it. Once you click the captio Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, Table, TableCaption, TableToolbar } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Table, TableToolbar, TableCaption, /* ... */ ], toolbar: [ 'insertTable', /* ... */ ], table: { diff --git a/packages/ckeditor5-table/docs/features/tables-resize.md b/packages/ckeditor5-table/docs/features/tables-resize.md index 40c943ffb9c..c4468c6efe7 100644 --- a/packages/ckeditor5-table/docs/features/tables-resize.md +++ b/packages/ckeditor5-table/docs/features/tables-resize.md @@ -31,13 +31,14 @@ The column resize feature is compatible with the {@link features/export-word Exp Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, Table, TableColumnResize } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Table, TableColumnResize, /* ... */ ], toolbar: [ 'insertTable', /* ... */ ], } ) diff --git a/packages/ckeditor5-table/docs/features/tables-styling.md b/packages/ckeditor5-table/docs/features/tables-styling.md index 412a8d0f271..36fe6d71d66 100644 --- a/packages/ckeditor5-table/docs/features/tables-styling.md +++ b/packages/ckeditor5-table/docs/features/tables-styling.md @@ -32,13 +32,14 @@ Put the caret anywhere inside the table to open the table toolbar. Click the tab Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, Table, TableCellProperties, TableProperties, TableToolbar } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Table, TableToolbar, TableProperties, TableCellProperties, /* ... */ ], toolbar: [ 'insertTable', /* ... */ ], table: { @@ -49,12 +50,10 @@ ClassicEditor tableProperties: { // The configuration of the TableProperties plugin. - // ... }, tableCellProperties: { // The configuration of the TableCellProperties plugin. - // ... } } } ) @@ -118,8 +117,7 @@ const customColorPalette = [ ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ Table, TableToolbar, TableProperties, TableCellProperties, Bold, /* ... */ ], - toolbar: [ 'insertTable', /* ... */ ], + // ... Other configuration options ... table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells', diff --git a/packages/ckeditor5-table/docs/features/tables.md b/packages/ckeditor5-table/docs/features/tables.md index 57eef982233..8dce9cbdb3f 100644 --- a/packages/ckeditor5-table/docs/features/tables.md +++ b/packages/ckeditor5-table/docs/features/tables.md @@ -187,15 +187,32 @@ The above model structure will be rendered to the data and to the editing view a Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, Table, TableToolbar } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Table, TableToolbar, Bold, /* ... */ ], toolbar: [ 'insertTable', /* ... */ ], + table: { + // Configuration. + } + } ) + .then( /* ... */ ) + .catch( /* ... */ ); +``` + +### Table contextual toolbar + +Easily control your tables employing a dedicated toolbar. + +```js +ClassicEditor + .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] } @@ -204,17 +221,15 @@ ClassicEditor .catch( /* ... */ ); ``` + ### Default table headers To make every inserted table have `n` number of rows and columns as table headers by default, set an optional table configuration property `defaultHeadings` as follows: ```js -import { ClassicEditor, Table, TableToolbar } from 'ckeditor5'; - ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ Table, TableToolbar, Bold, /* ... */ ], - toolbar: [ 'insertTable', /* ... */ ], + // ... Other configuration options ... table: { defaultHeadings: { rows: 1, columns: 1 } } @@ -246,9 +261,8 @@ function DisallowNestingTables( editor ) { ClassicEditor .create( document.querySelector( '#editor' ), { + // ... Other configuration options ... extraPlugins: [ DisallowNestingTables ], - - // The rest of the configuration. } ) .then( /* ... */ ) .catch( /* ... */ ); diff --git a/packages/ckeditor5-theme-lark/docs/_snippets/examples/theme-lark.js b/packages/ckeditor5-theme-lark/docs/_snippets/examples/theme-lark.js index b48ec196912..6330c53e51c 100644 --- a/packages/ckeditor5-theme-lark/docs/_snippets/examples/theme-lark.js +++ b/packages/ckeditor5-theme-lark/docs/_snippets/examples/theme-lark.js @@ -34,7 +34,8 @@ ClassicEditor image: { toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:wrapText', '|', 'toggleImageCaption', 'imageTextAlternative' ] }, - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-theme-lark/docs/framework/theme-customization.md b/packages/ckeditor5-theme-lark/docs/framework/theme-customization.md index 9c05f851035..aa494be74cf 100644 --- a/packages/ckeditor5-theme-lark/docs/framework/theme-customization.md +++ b/packages/ckeditor5-theme-lark/docs/framework/theme-customization.md @@ -12,7 +12,7 @@ Below you can see a demo of an editor with the dark theme as a result of customi ## Customization with CSS variables -Assuming you finished our {@link getting-started/quick-start quick start} guide, and you have a running CKEditor 5 instance, let's use the full potential of CSS variables (custom properties). The customization explained in this guide will make the theme dark, with slightly bigger text and more rounded corners. +Assuming you finished our {@link getting-started/integrations-cdn/quick-start quick start} guide, and you have a running CKEditor 5 instance, let's use the full potential of CSS variables (custom properties). The customization explained in this guide will make the theme dark, with slightly bigger text and more rounded corners. The file containing custom variables can be named `custom.css` and it will look as below: diff --git a/packages/ckeditor5-typing/docs/_snippets/features/text-transformation-extended.js b/packages/ckeditor5-typing/docs/_snippets/features/text-transformation-extended.js index 6897318b899..16d23edd3f0 100644 --- a/packages/ckeditor5-typing/docs/_snippets/features/text-transformation-extended.js +++ b/packages/ckeditor5-typing/docs/_snippets/features/text-transformation-extended.js @@ -72,7 +72,8 @@ ClassicEditor } ] } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-typing/docs/_snippets/features/text-transformation.js b/packages/ckeditor5-typing/docs/_snippets/features/text-transformation.js index bd8535eb774..e11f16f0ba0 100644 --- a/packages/ckeditor5-typing/docs/_snippets/features/text-transformation.js +++ b/packages/ckeditor5-typing/docs/_snippets/features/text-transformation.js @@ -35,7 +35,8 @@ ClassicEditor tokenUrl: TOKEN_URL, allowExternalImagesEditing: [ /^data:/, 'origin', /ckbox/ ], forceDemoLabel: true - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-typing/docs/features/text-transformation.md b/packages/ckeditor5-typing/docs/features/text-transformation.md index a76f57a7f6e..520861a3ee9 100644 --- a/packages/ckeditor5-typing/docs/features/text-transformation.md +++ b/packages/ckeditor5-typing/docs/features/text-transformation.md @@ -71,14 +71,18 @@ You may find interesting details and usage examples in the [Automatic text trans Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, TextTransformation } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ TextTransformation, /* ... */ ], + typing: { + // Configuration. + } } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -101,6 +105,7 @@ For instance, to use the transformations from the "quotes" and "typography" grou ```js ClassicEditor .create( editorElement, { + // ... Other configuration options ... typing: { transformations: { include: [ @@ -125,6 +130,7 @@ Another example, removing some transformations and adding some extra ones: ```js ClassicEditor .create( editorElement, { + // ... Other configuration options ... typing: { transformations: { remove: [ diff --git a/packages/ckeditor5-ui/docs/_snippets/examples/bootstrap-ui-inner.js b/packages/ckeditor5-ui/docs/_snippets/examples/bootstrap-ui-inner.js index 7a0ed697503..8868fa37336 100644 --- a/packages/ckeditor5-ui/docs/_snippets/examples/bootstrap-ui-inner.js +++ b/packages/ckeditor5-ui/docs/_snippets/examples/bootstrap-ui-inner.js @@ -281,7 +281,8 @@ BootstrapEditor Clipboard, Enter, Typing, Paragraph, EasyImage, Image, ImageUpload, CloudServices, BoldEditing, ItalicEditing, UnderlineEditing, HeadingEditing, UndoEditing ], - cloudServices: CS_CONFIG + cloudServices: CS_CONFIG, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-ui/src/badge/badge.ts b/packages/ckeditor5-ui/src/badge/badge.ts new file mode 100644 index 00000000000..fe64bba2b86 --- /dev/null +++ b/packages/ckeditor5-ui/src/badge/badge.ts @@ -0,0 +1,346 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module ui/badge/badge + */ + +import type { Editor } from '@ckeditor/ckeditor5-core'; + +import { + Rect, + DomEmitterMixin, + type PositionOptions +} from '@ckeditor/ckeditor5-utils'; + +import type View from '../view.js'; +import BalloonPanelView from '../panel/balloon/balloonpanelview.js'; + +import { throttle } from 'lodash-es'; + +// ⚠ Note, whenever changing the threshold, make sure to update the docs/support/managing-ckeditor-logo.md docs +// as this information is also mentioned there ⚠. +const NARROW_ROOT_HEIGHT_THRESHOLD = 50; +const NARROW_ROOT_WIDTH_THRESHOLD = 350; + +/** + * A helper that enables the badge feature in the editor and renders a custom view next to the bottom of the editable element + * (editor root, source editing area, etc.) when the editor is focused. + * + * @private + */ +export default abstract class Badge extends /* #__PURE__ */ DomEmitterMixin() { + /** + * Editor instance the helper was created for. + */ + protected readonly editor: Editor; + + /** + * A reference to the balloon panel hosting and positioning the badge content. + */ + private _balloonView: BalloonPanelView | null = null; + + /** + * A throttled version of the {@link #_showBalloon} method meant for frequent use to avoid performance loss. + */ + private _showBalloonThrottled = throttle( () => this._showBalloon(), 50, { leading: true } ); + + /** + * A reference to the last editable element (root, source editing area, etc.) focused by the user. + * Since the focus can move to other focusable elements in the UI, this reference allows positioning the balloon over the + * right element whether the user is typing or using the UI. + */ + private _lastFocusedEditableElement: HTMLElement | null = null; + + /** + * An additional CSS class added to the `BalloonView`. + */ + private readonly _balloonClass: string | undefined; + + /** + * Creates a badge for a given editor. The feature is initialized on Editor#ready + * event. + */ + protected constructor( editor: Editor, options: { balloonClass?: string } = {} ) { + super(); + + this.editor = editor; + this._balloonClass = options.balloonClass; + + editor.on( 'ready', () => this._handleEditorReady() ); + } + + /** + * Destroys the badge along with its view. + */ + public destroy(): void { + const balloon = this._balloonView; + + if ( balloon ) { + // Balloon gets destroyed by the body collection. + // The badge view gets destroyed by the balloon. + balloon.unpin(); + this._balloonView = null; + } + + this._showBalloonThrottled.cancel(); + this.stopListening(); + } + + /** + * Enables badge label once the editor (ui) is ready. + */ + protected _handleEditorReady(): void { + const editor = this.editor; + + if ( !this._isEnabled() ) { + return; + } + + // No view means no body collection to append the badge balloon to. + if ( !editor.ui.view ) { + return; + } + + editor.ui.focusTracker.on( 'change:isFocused', ( evt, data, isFocused ) => { + this._updateLastFocusedEditableElement(); + + if ( isFocused ) { + this._showBalloon(); + } else { + this._hideBalloon(); + } + } ); + + editor.ui.focusTracker.on( 'change:focusedElement', ( evt, data, focusedElement ) => { + this._updateLastFocusedEditableElement(); + + if ( focusedElement ) { + this._showBalloon(); + } + } ); + + editor.ui.on( 'update', () => { + this._showBalloonThrottled(); + } ); + } + + /** + * Returns normalized configuration for the badge. + */ + protected _getNormalizedConfig(): BadgeConfig { + return { + side: this.editor.locale.contentLanguageDirection === 'ltr' ? 'right' : 'left', + position: 'border', + verticalOffset: 0, + horizontalOffset: 5 + }; + } + + /** + * Creates the badge content. + */ + protected abstract _createBadgeContent(): View; + + /** + * Enables the badge feature. + */ + protected abstract _isEnabled(): boolean; + + /** + * Attempts to display the balloon with the badge view. + */ + private _showBalloon(): void { + const attachOptions = this._getBalloonAttachOptions(); + + if ( !attachOptions ) { + return; + } + + if ( !this._balloonView ) { + this._balloonView = this._createBalloonView(); + } + + this._balloonView.pin( attachOptions ); + } + + /** + * Hides the badge balloon if already visible. + */ + private _hideBalloon(): void { + if ( this._balloonView ) { + this._balloonView.unpin(); + } + } + + /** + * Creates an instance of the {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView balloon panel} + * with the badge view inside ready for positioning. + */ + private _createBalloonView(): BalloonPanelView { + const editor = this.editor; + const balloon = new BalloonPanelView(); + const view = this._createBadgeContent(); + + balloon.content.add( view ); + + if ( this._balloonClass ) { + balloon.class = this._balloonClass; + } + + editor.ui.view.body.add( balloon ); + + return balloon; + } + + /** + * Returns the options for attaching the balloon to the focused editable element. + */ + private _getBalloonAttachOptions(): Partial | null { + if ( !this._lastFocusedEditableElement ) { + return null; + } + + const badgeConfig = this._getNormalizedConfig(); + + const positioningFunction = badgeConfig.side === 'right' ? + getLowerRightCornerPosition( this._lastFocusedEditableElement, badgeConfig ) : + getLowerLeftCornerPosition( this._lastFocusedEditableElement, badgeConfig ); + + return { + target: this._lastFocusedEditableElement, + positions: [ positioningFunction ] + }; + } + + /** + * Updates the {@link #_lastFocusedEditableElement} based on the state of the global focus tracker. + */ + private _updateLastFocusedEditableElement(): void { + const editor = this.editor; + const isFocused = editor.ui.focusTracker.isFocused; + const focusedElement = editor.ui.focusTracker.focusedElement! as HTMLElement; + + if ( !isFocused || !focusedElement ) { + this._lastFocusedEditableElement = null; + + return; + } + + const editableEditorElements = Array.from( editor.ui.getEditableElementsNames() ).map( name => { + return editor.ui.getEditableElement( name ); + } ); + + if ( editableEditorElements.includes( focusedElement ) ) { + this._lastFocusedEditableElement = focusedElement; + } else { + // If it's none of the editable element, then the focus is somewhere in the UI. Let's display the badge + // over the first element then. + this._lastFocusedEditableElement = editableEditorElements[ 0 ]!; + } + } +} + +function getLowerRightCornerPosition( focusedEditableElement: HTMLElement, config: BadgeConfig ) { + return getLowerCornerPosition( focusedEditableElement, config, ( rootRect, balloonRect ) => { + return rootRect.left + rootRect.width - balloonRect.width - config.horizontalOffset; + } ); +} + +function getLowerLeftCornerPosition( focusedEditableElement: HTMLElement, config: BadgeConfig ) { + return getLowerCornerPosition( focusedEditableElement, config, rootRect => rootRect.left + config.horizontalOffset ); +} + +function getLowerCornerPosition( + focusedEditableElement: HTMLElement, + config: BadgeConfig, + getBalloonLeft: ( visibleEditableElementRect: Rect, balloonRect: Rect ) => number +) { + return ( visibleEditableElementRect: Rect, balloonRect: Rect ) => { + const editableElementRect = new Rect( focusedEditableElement ); + + if ( editableElementRect.width < NARROW_ROOT_WIDTH_THRESHOLD || editableElementRect.height < NARROW_ROOT_HEIGHT_THRESHOLD ) { + return null; + } + + let balloonTop; + + if ( config.position === 'inside' ) { + balloonTop = editableElementRect.bottom - balloonRect.height; + } + else { + balloonTop = editableElementRect.bottom - balloonRect.height / 2; + } + + balloonTop -= config.verticalOffset; + + const balloonLeft = getBalloonLeft( editableElementRect, balloonRect ); + + // Clone the editable element rect and place it where the balloon would be placed. + // This will allow getVisible() to work from editable element's perspective (rect source). + // and yield a result as if the balloon was on the same (scrollable) layer as the editable element. + const newBalloonPositionRect = visibleEditableElementRect + .clone() + .moveTo( balloonLeft, balloonTop ) + .getIntersection( balloonRect.clone().moveTo( balloonLeft, balloonTop ) )!; + + const newBalloonPositionVisibleRect = newBalloonPositionRect.getVisible(); + + if ( !newBalloonPositionVisibleRect || newBalloonPositionVisibleRect.getArea() < balloonRect.getArea() ) { + return null; + } + + return { + top: balloonTop, + left: balloonLeft, + name: `position_${ config.position }-side_${ config.side }`, + config: { + withArrow: false + } + }; + }; +} + +/** + * The badge configuration options. + **/ +export interface BadgeConfig { + + /** + * The position of the badge. + * + * * When `'inside'`, the badge will be displayed within the boundaries of the editing area. + * * When `'border'`, the basge will be displayed over the bottom border of the editing area. + * + * @default 'border' + */ + position: 'inside' | 'border'; + + /** + * Allows choosing the side of the editing area where the badge will be displayed. + * + * **Note:** If {@link module:core/editor/editorconfig~EditorConfig#language `config.language`} is set to an RTL (right-to-left) + * language, the side switches to `'left'` by default. + * + * @default 'right' + */ + side: 'left' | 'right'; + + /** + * The vertical distance the badge can be moved away from its default position. + * + * **Note:** If `position` is `'border'`, the offset is measured from the (vertical) center of the badge. + * + * @default 5 + */ + verticalOffset: number; + + /** + * The horizontal distance between the side of the editing root and the nearest side of the badge. + * + * @default 5 + */ + horizontalOffset: number; +} diff --git a/packages/ckeditor5-ui/src/editorui/editorui.ts b/packages/ckeditor5-ui/src/editorui/editorui.ts index 21851581a36..f635555560c 100644 --- a/packages/ckeditor5-ui/src/editorui/editorui.ts +++ b/packages/ckeditor5-ui/src/editorui/editorui.ts @@ -12,6 +12,7 @@ import ComponentFactory from '../componentfactory.js'; import TooltipManager from '../tooltipmanager.js'; import PoweredBy from './poweredby.js'; +import EvaluationBadge from './evaluationbadge.js'; import AriaLiveAnnouncer from '../arialiveannouncer.js'; import type EditorUIView from './editoruiview.js'; @@ -66,6 +67,11 @@ export default abstract class EditorUI extends /* #__PURE__ */ ObservableMixin() */ public readonly poweredBy: PoweredBy; + /** + * A helper that enables the "evaluation badge" feature in the editor. + */ + public readonly evaluationBadge: EvaluationBadge; + /** * A helper that manages the content of an `aria-live` regions used by editor features to announce status changes * to screen readers. @@ -154,6 +160,7 @@ export default abstract class EditorUI extends /* #__PURE__ */ ObservableMixin() this.focusTracker = new FocusTracker(); this.tooltipManager = new TooltipManager( editor ); this.poweredBy = new PoweredBy( editor ); + this.evaluationBadge = new EvaluationBadge( editor ); this.ariaLiveAnnouncer = new AriaLiveAnnouncer( editor ); this.set( 'viewportOffset', this._readViewportOffsetFromConfig() ); @@ -206,6 +213,7 @@ export default abstract class EditorUI extends /* #__PURE__ */ ObservableMixin() this.focusTracker.destroy(); this.tooltipManager.destroy( this.editor ); this.poweredBy.destroy(); + this.evaluationBadge.destroy(); // Clean–up the references to the CKEditor instance stored in the native editable DOM elements. for ( const domElement of this._editableElementsMap.values() ) { diff --git a/packages/ckeditor5-ui/src/editorui/evaluationbadge.ts b/packages/ckeditor5-ui/src/editorui/evaluationbadge.ts new file mode 100644 index 00000000000..182c2ac6c48 --- /dev/null +++ b/packages/ckeditor5-ui/src/editorui/evaluationbadge.ts @@ -0,0 +1,120 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module ui/editorui/evaluationbadge + */ + +import type { Editor } from '@ckeditor/ckeditor5-core'; +import { parseBase64EncodedObject, type Locale } from '@ckeditor/ckeditor5-utils'; + +import View from '../view.js'; +import Badge, { type BadgeConfig } from '../badge/badge.js'; + +/** + * A helper that enables the "evaluation badge" feature in the editor at the bottom of the editable element + * (editor root, source editing area, etc.) when the editor is focused. + * + * @private + */ +export default class EvaluationBadge extends Badge { + private licenseTypeMessage: Record = { + evaluation: 'For evaluation purposes only', + trial: 'For evaluation purposes only', + development: 'For development purposes only' + }; + + constructor( editor: Editor ) { + super( editor, { balloonClass: 'ck-evaluation-badge-balloon' } ); + } + + /** + * Enables "evaluation badge" label. + */ + protected override _isEnabled(): boolean { + const editor = this.editor; + const licenseKey = editor.config.get( 'licenseKey' )!; + const licenseType = getLicenseTypeFromLicenseKey( licenseKey ); + + return Boolean( licenseType && this.licenseTypeMessage[ licenseType ] ); + } + + /** + * Creates the content of the "evaluation badge". + */ + protected override _createBadgeContent(): View { + const licenseKey = this.editor.config.get( 'licenseKey' )!; + const licenseType = getLicenseTypeFromLicenseKey( licenseKey )!; + + return new EvaluationBadgeView( this.editor.locale, this.licenseTypeMessage[ licenseType ] ); + } + + /** + * Returns the normalized configuration for the "evaluation badge". + * It takes 'ui.poweredBy' configuration into account to determine the badge position and side. + */ + protected override _getNormalizedConfig(): BadgeConfig { + const badgeConfig = super._getNormalizedConfig(); + const userConfig = this.editor.config.get( 'ui.poweredBy' ) || {}; + const position = userConfig.position || badgeConfig.position; + const poweredBySide = userConfig.side || badgeConfig.side; + + return { + position, + side: poweredBySide === 'left' ? 'right' : 'left', + verticalOffset: badgeConfig.verticalOffset, + horizontalOffset: badgeConfig.horizontalOffset + }; + } +} + +/** + * A view displaying the "evaluation badge". + */ +class EvaluationBadgeView extends View { + /** + * Creates an instance of the "evaluation badge" view. + * + * @param locale The localization services instance. + * @param label The label text. + */ + constructor( locale: Locale, label: string ) { + super( locale ); + + this.setTemplate( { + tag: 'div', + attributes: { + class: [ 'ck', 'ck-evaluation-badge' ], + 'aria-hidden': true + }, + children: [ + { + tag: 'span', + attributes: { + class: [ 'ck', 'ck-evaluation-badge__label' ] + }, + children: [ label ] + } + ] + } ); + } +} + +/** + * Returns the license type based on the license key. + */ +function getLicenseTypeFromLicenseKey( licenseKey: string ): string | null { + if ( licenseKey == 'GPL' ) { + return 'GPL'; + } + + const licenseContent = parseBase64EncodedObject( licenseKey.split( '.' )[ 1 ] ); + + if ( !licenseContent ) { + return null; + } + + return licenseContent.licenseType || 'production'; +} diff --git a/packages/ckeditor5-ui/src/editorui/poweredby.ts b/packages/ckeditor5-ui/src/editorui/poweredby.ts index 6f75ddd1357..cff7de48f78 100644 --- a/packages/ckeditor5-ui/src/editorui/poweredby.ts +++ b/packages/ckeditor5-ui/src/editorui/poweredby.ts @@ -8,26 +8,17 @@ */ import type { Editor, UiConfig } from '@ckeditor/ckeditor5-core'; -import { - DomEmitterMixin, - Rect, - verifyLicense, - type PositionOptions, - type Locale -} from '@ckeditor/ckeditor5-utils'; -import BalloonPanelView from '../panel/balloon/balloonpanelview.js'; -import IconView from '../icon/iconview.js'; +import { parseBase64EncodedObject, type Locale } from '@ckeditor/ckeditor5-utils'; + import View from '../view.js'; -import { throttle, type DebouncedFunc } from 'lodash-es'; +import Badge from '../badge/badge.js'; +import IconView from '../icon/iconview.js'; import poweredByIcon from '../../theme/icons/project-logo.svg'; const ICON_WIDTH = 53; const ICON_HEIGHT = 10; -// ⚠ Note, whenever changing the threshold, make sure to update the docs/support/managing-ckeditor-logo.md docs -// as this information is also mentioned there ⚠. -const NARROW_ROOT_HEIGHT_THRESHOLD = 50; -const NARROW_ROOT_WIDTH_THRESHOLD = 350; + const DEFAULT_LABEL = 'Powered by'; type PoweredByConfig = Required[ 'poweredBy' ]; @@ -38,176 +29,62 @@ type PoweredByConfig = Required[ 'poweredBy' ]; * * @private */ -export default class PoweredBy extends /* #__PURE__ */ DomEmitterMixin() { - /** - * Editor instance the helper was created for. - */ - private readonly editor: Editor; - - /** - * A reference to the balloon panel hosting and positioning the "powered by" link and logo. - */ - private _balloonView: BalloonPanelView | null; - - /** - * A throttled version of the {@link #_showBalloon} method meant for frequent use to avoid performance loss. - */ - private _showBalloonThrottled: DebouncedFunc<() => void>; - - /** - * A reference to the last editable element (root, source editing area, etc.) focused by the user. - * Since the focus can move to other focusable elements in the UI, this reference allows positioning the balloon over the - * right element whether the user is typing or using the UI. - */ - private _lastFocusedEditableElement: HTMLElement | null; - - /** - * Creates a "powered by" helper for a given editor. The feature is initialized on Editor#ready - * event. - * - * @param editor - */ +export default class PoweredBy extends Badge { constructor( editor: Editor ) { - super(); - - this.editor = editor; - this._balloonView = null; - this._lastFocusedEditableElement = null; - this._showBalloonThrottled = throttle( this._showBalloon.bind( this ), 50, { leading: true } ); - - editor.on( 'ready', this._handleEditorReady.bind( this ) ); + super( editor, { balloonClass: 'ck-powered-by-balloon' } ); } /** - * Destroys the "powered by" helper along with its view. + * Enables "powered by" label. */ - public destroy(): void { - const balloon = this._balloonView; - - if ( balloon ) { - // Balloon gets destroyed by the body collection. - // The powered by view gets destroyed by the balloon. - balloon.unpin(); - this._balloonView = null; - } - - this._showBalloonThrottled.cancel(); - this.stopListening(); - } - - /** - * Enables "powered by" label once the editor (ui) is ready. - */ - private _handleEditorReady(): void { + protected override _isEnabled(): boolean { const editor = this.editor; - const forceVisible = !!editor.config.get( 'ui.poweredBy.forceVisible' ); - - /* istanbul ignore next -- @preserve */ - if ( !forceVisible && verifyLicense( editor.config.get( 'licenseKey' ) ) === 'VALID' ) { - return; - } + const forceVisible = editor.config.get( 'ui.poweredBy.forceVisible' ); - // No view means no body collection to append the powered by balloon to. - if ( !editor.ui.view ) { - return; + if ( forceVisible ) { + return true; } - editor.ui.focusTracker.on( 'change:isFocused', ( evt, data, isFocused ) => { - this._updateLastFocusedEditableElement(); - - if ( isFocused ) { - this._showBalloon(); - } else { - this._hideBalloon(); - } - } ); - - editor.ui.focusTracker.on( 'change:focusedElement', ( evt, data, focusedElement ) => { - this._updateLastFocusedEditableElement(); - - if ( focusedElement ) { - this._showBalloon(); - } - } ); - - editor.ui.on( 'update', () => { - this._showBalloonThrottled(); - } ); - } - - /** - * Creates an instance of the {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView balloon panel} - * with the "powered by" view inside ready for positioning. - */ - private _createBalloonView(): void { - const editor = this.editor; - const balloon = this._balloonView = new BalloonPanelView(); - const poweredByConfig = getNormalizedConfig( editor ); - const view = new PoweredByView( editor.locale, poweredByConfig.label ); - - balloon.content.add( view ); - balloon.set( { - class: 'ck-powered-by-balloon' - } ); - - editor.ui.view.body.add( balloon ); + const licenseKey = editor.config.get( 'licenseKey' )!; - this._balloonView = balloon; - } - - /** - * Attempts to display the balloon with the "powered by" view. - */ - private _showBalloon(): void { - if ( !this._lastFocusedEditableElement ) { - return; + if ( licenseKey == 'GPL' ) { + return true; } - const attachOptions = getBalloonAttachOptions( this.editor, this._lastFocusedEditableElement ); - - if ( attachOptions ) { - if ( !this._balloonView ) { - this._createBalloonView(); - } + const licenseContent = parseBase64EncodedObject( licenseKey.split( '.' )[ 1 ] ); - this._balloonView!.pin( attachOptions ); + if ( !licenseContent ) { + return true; } + + return !licenseContent.whiteLabel; } /** - * Hides the "powered by" balloon if already visible. + * Creates a "powered by" badge content. */ - private _hideBalloon(): void { - if ( this._balloonView ) { - this._balloonView!.unpin(); - } + protected override _createBadgeContent(): View { + return new PoweredByView( this.editor.locale, this._getNormalizedConfig().label ); } /** - * Updates the {@link #_lastFocusedEditableElement} based on the state of the global focus tracker. + * Returns the normalized configuration for the "powered by" badge. + * It takes the user configuration into account and falls back to the default one. */ - private _updateLastFocusedEditableElement(): void { - const editor = this.editor; - const isFocused = editor.ui.focusTracker.isFocused; - const focusedElement = editor.ui.focusTracker.focusedElement! as HTMLElement; - - if ( !isFocused || !focusedElement ) { - this._lastFocusedEditableElement = null; - - return; - } + protected override _getNormalizedConfig(): Required { + const badgeConfig = super._getNormalizedConfig(); + const userConfig = this.editor.config.get( 'ui.poweredBy' ) || {}; + const position = userConfig.position || badgeConfig.position; + const verticalOffset = position === 'inside' ? 5 : badgeConfig.verticalOffset; - const editableEditorElements = Array.from( editor.ui.getEditableElementsNames() ).map( name => { - return editor.ui.getEditableElement( name ); - } ); - - if ( editableEditorElements.includes( focusedElement ) ) { - this._lastFocusedEditableElement = focusedElement; - } else { - // If it's none of the editable element, then the focus is somewhere in the UI. Let's display powered by - // over the first element then. - this._lastFocusedEditableElement = editableEditorElements[ 0 ]!; - } + return { + position, + side: userConfig.side || badgeConfig.side, + label: userConfig.label === undefined ? DEFAULT_LABEL : userConfig.label, + verticalOffset: userConfig.verticalOffset !== undefined ? userConfig.verticalOffset : verticalOffset, + horizontalOffset: userConfig.horizontalOffset !== undefined ? userConfig.horizontalOffset : badgeConfig.horizontalOffset, + forceVisible: !!userConfig.forceVisible + }; } } @@ -216,7 +93,7 @@ export default class PoweredBy extends /* #__PURE__ */ DomEmitterMixin() { */ class PoweredByView extends View { /** - * Created an instance of the "powered by" view. + * Creates an instance of the "powered by" view. * * @param locale The localization services instance. * @param label The label text. @@ -276,90 +153,3 @@ class PoweredByView extends View { } ); } } - -function getBalloonAttachOptions( editor: Editor, focusedEditableElement: HTMLElement ): Partial | null { - const poweredByConfig = getNormalizedConfig( editor )!; - const positioningFunction = poweredByConfig.side === 'right' ? - getLowerRightCornerPosition( focusedEditableElement, poweredByConfig ) : - getLowerLeftCornerPosition( focusedEditableElement, poweredByConfig ); - - return { - target: focusedEditableElement, - positions: [ positioningFunction ] - }; -} - -function getLowerRightCornerPosition( focusedEditableElement: HTMLElement, config: PoweredByConfig ) { - return getLowerCornerPosition( focusedEditableElement, config, ( rootRect, balloonRect ) => { - return rootRect.left + rootRect.width - balloonRect.width - config.horizontalOffset; - } ); -} - -function getLowerLeftCornerPosition( focusedEditableElement: HTMLElement, config: PoweredByConfig ) { - return getLowerCornerPosition( focusedEditableElement, config, rootRect => rootRect.left + config.horizontalOffset ); -} - -function getLowerCornerPosition( - focusedEditableElement: HTMLElement, - config: PoweredByConfig, - getBalloonLeft: ( visibleEditableElementRect: Rect, balloonRect: Rect ) => number -) { - return ( visibleEditableElementRect: Rect, balloonRect: Rect ) => { - const editableElementRect = new Rect( focusedEditableElement ); - - if ( editableElementRect.width < NARROW_ROOT_WIDTH_THRESHOLD || editableElementRect.height < NARROW_ROOT_HEIGHT_THRESHOLD ) { - return null; - } - - let balloonTop; - - if ( config.position === 'inside' ) { - balloonTop = editableElementRect.bottom - balloonRect.height; - } - else { - balloonTop = editableElementRect.bottom - balloonRect.height / 2; - } - - balloonTop -= config.verticalOffset; - - const balloonLeft = getBalloonLeft( editableElementRect, balloonRect ); - - // Clone the editable element rect and place it where the balloon would be placed. - // This will allow getVisible() to work from editable element's perspective (rect source). - // and yield a result as if the balloon was on the same (scrollable) layer as the editable element. - const newBalloonPositionRect = visibleEditableElementRect - .clone() - .moveTo( balloonLeft, balloonTop ) - .getIntersection( balloonRect.clone().moveTo( balloonLeft, balloonTop ) )!; - - const newBalloonPositionVisibleRect = newBalloonPositionRect.getVisible(); - - if ( !newBalloonPositionVisibleRect || newBalloonPositionVisibleRect.getArea() < balloonRect.getArea() ) { - return null; - } - - return { - top: balloonTop, - left: balloonLeft, - name: `position_${ config.position }-side_${ config.side }`, - config: { - withArrow: false - } - }; - }; -} - -function getNormalizedConfig( editor: Editor ): PoweredByConfig { - const userConfig = editor.config.get( 'ui.poweredBy' ); - const position = userConfig && userConfig.position || 'border'; - - return { - position, - label: DEFAULT_LABEL, - verticalOffset: position === 'inside' ? 5 : 0, - horizontalOffset: 5, - - side: editor.locale.contentLanguageDirection === 'ltr' ? 'right' : 'left', - ...userConfig - }; -} diff --git a/packages/ckeditor5-ui/tests/badge/badge.js b/packages/ckeditor5-ui/tests/badge/badge.js new file mode 100644 index 00000000000..a5a82891a3c --- /dev/null +++ b/packages/ckeditor5-ui/tests/badge/badge.js @@ -0,0 +1,666 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* global document, window, HTMLElement */ + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor.js'; +import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting.js'; +import { Rect, global } from '@ckeditor/ckeditor5-utils'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils.js'; + +import { BalloonPanelView } from '../../src/index.js'; +import View from '../../src/view.js'; +import Badge from '../../src/badge/badge.js'; + +class BadgeExtended extends Badge { + _isEnabled() { + return true; + } + + _createBadgeContent() { + return new EvaluationBadgeView( this.editor.locale, 'Badge extended label' ); + } +} + +class EvaluationBadgeView extends View { + constructor( locale, label ) { + super( locale ); + + this.setTemplate( { + tag: 'div', + attributes: { + class: [ 'ck-badge-extended' ] + }, + children: [ + { + tag: 'span', + attributes: { + class: [ 'ck-badge-extended__label' ] + }, + children: [ label ] + } + ] + } ); + } +} + +describe( 'Badge', () => { + let editor, element, badge; + + testUtils.createSinonSandbox(); + + beforeEach( async () => { + element = document.createElement( 'div' ); + document.body.appendChild( element ); + editor = await createEditor( element ); + + badge = new BadgeExtended( editor ); + editor.fire( 'ready' ); + + testUtils.sinon.stub( editor.editing.view.getDomRoot(), 'getBoundingClientRect' ).returns( { + top: 0, + left: 0, + right: 400, + width: 400, + bottom: 100, + height: 100 + } ); + + testUtils.sinon.stub( document.body, 'getBoundingClientRect' ).returns( { + top: 0, + right: 1000, + bottom: 1000, + left: 0, + width: 1000, + height: 1000 + } ); + + sinon.stub( global.window, 'innerWidth' ).value( 1000 ); + sinon.stub( global.window, 'innerHeight' ).value( 1000 ); + } ); + + afterEach( async () => { + element.remove(); + await editor.destroy(); + } ); + + describe( 'constructor()', () => { + describe( 'balloon creation', () => { + it( 'should create the balloon on demand', () => { + expect( badge._balloonView ).to.be.null; + + focusEditor( editor ); + + expect( badge._balloonView ).to.be.instanceOf( BalloonPanelView ); + } ); + } ); + + describe( 'balloon management on editor focus change', () => { + const originalGetVisible = Rect.prototype.getVisible; + + it( 'should show the balloon when the editor gets focused', () => { + focusEditor( editor ); + + expect( badge._balloonView.isVisible ).to.be.true; + } ); + + it( 'should show the balloon if the focus is not in the editing root but in other editor UI', async () => { + const focusableEditorUIElement = document.createElement( 'input' ); + focusableEditorUIElement.type = 'text'; + document.body.appendChild( focusableEditorUIElement ); + + editor.ui.focusTracker.add( focusableEditorUIElement ); + + // Just generate the balloon on demand. + focusEditor( editor ); + blurEditor( editor ); + + await wait( 10 ); + const pinSpy = testUtils.sinon.spy( badge._balloonView, 'pin' ); + + focusEditor( editor, focusableEditorUIElement ); + + sinon.assert.calledOnce( pinSpy ); + sinon.assert.calledWith( pinSpy, sinon.match.has( 'target', editor.editing.view.getDomRoot() ) ); + + focusableEditorUIElement.remove(); + } ); + + it( 'should hide the balloon on blur', async () => { + focusEditor( editor ); + + expect( badge._balloonView.isVisible ).to.be.true; + + blurEditor( editor ); + + // FocusTracker's blur handler is asynchronous. + await wait( 200 ); + + expect( badge._balloonView.isVisible ).to.be.false; + } ); + + // This is a weak test because it does not check the geometry but it will do. + it( 'should show the balloon when the source editing is engaged', async () => { + const domRoot = editor.editing.view.getDomRoot(); + const originalGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect; + + function isEditableElement( element ) { + return Array.from( editor.ui.getEditableElementsNames() ).map( name => { + return editor.ui.getEditableElement( name ); + } ).includes( element ); + } + + // Rect#getVisible() passthrough to ignore ancestors. Makes testing a lot easier. + testUtils.sinon.stub( Rect.prototype, 'getVisible' ).callsFake( function() { + if ( isEditableElement( this._source ) ) { + return new Rect( this._source ); + } else { + return originalGetVisible.call( this ); + } + } ); + + // Stub textarea's client rect. + testUtils.sinon.stub( HTMLElement.prototype, 'getBoundingClientRect' ).callsFake( function() { + if ( this.parentNode.classList.contains( 'ck-source-editing-area' ) ) { + return { + top: 0, + left: 0, + right: 400, + width: 400, + bottom: 200, + height: 200 + }; + } + + return originalGetBoundingClientRect.call( this ); + } ); + + focusEditor( editor ); + + domRoot.getBoundingClientRect.returns( { + top: 0, + left: 0, + right: 350, + width: 350, + bottom: 100, + height: 100 + } ); + + const pinSpy = testUtils.sinon.spy( badge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + await wait( 75 ); + + expect( badge._balloonView.isVisible ).to.be.true; + expect( badge._balloonView.position ).to.equal( 'position_border-side_right' ); + sinon.assert.calledWith( pinSpy.lastCall, sinon.match.has( 'target', domRoot ) ); + + editor.plugins.get( 'SourceEditing' ).isSourceEditingMode = true; + + const sourceAreaElement = editor.ui.getEditableElement( 'sourceEditing:main' ); + + focusEditor( editor, sourceAreaElement ); + sinon.assert.calledWith( + pinSpy.lastCall, + sinon.match.has( 'target', sourceAreaElement ) + ); + + expect( badge._balloonView.isVisible ).to.be.true; + expect( badge._balloonView.position ).to.equal( 'position_border-side_right' ); + + editor.plugins.get( 'SourceEditing' ).isSourceEditingMode = false; + focusEditor( editor ); + + expect( badge._balloonView.isVisible ).to.be.true; + expect( badge._balloonView.position ).to.equal( 'position_border-side_right' ); + sinon.assert.calledWith( pinSpy.lastCall, sinon.match.has( 'target', domRoot ) ); + } ); + } ); + + describe( 'balloon management on EditorUI#update', () => { + it( 'should not trigger if the editor is not focused', () => { + expect( badge._balloonView ).to.be.null; + + editor.ui.fire( 'update' ); + + expect( badge._balloonView ).to.be.null; + } ); + + it( 'should (re-)show the balloon but throttled', async () => { + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( badge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + editor.ui.fire( 'update' ); + + sinon.assert.notCalled( pinSpy ); + + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + sinon.assert.calledWith( pinSpy.firstCall, sinon.match.has( 'target', editor.editing.view.getDomRoot() ) ); + } ); + + it( 'should (re-)show the balloon if the focus is not in the editing root but in other editor UI', async () => { + const focusableEditorUIElement = document.createElement( 'input' ); + focusableEditorUIElement.type = 'text'; + editor.ui.focusTracker.add( focusableEditorUIElement ); + document.body.appendChild( focusableEditorUIElement ); + + focusEditor( editor, focusableEditorUIElement ); + + const pinSpy = testUtils.sinon.spy( badge._balloonView, 'pin' ); + + sinon.assert.notCalled( pinSpy ); + + editor.ui.fire( 'update' ); + editor.ui.fire( 'update' ); + + sinon.assert.calledOnce( pinSpy ); + + await wait( 75 ); + + sinon.assert.calledTwice( pinSpy ); + sinon.assert.calledWith( pinSpy, sinon.match.has( 'target', editor.editing.view.getDomRoot() ) ); + focusableEditorUIElement.remove(); + } ); + } ); + + describe( 'balloon view', () => { + let balloon, focusTrackerAddSpy; + + beforeEach( () => { + focusTrackerAddSpy = testUtils.sinon.spy( editor.ui.focusTracker, 'add' ); + + focusEditor( editor ); + + balloon = badge._balloonView; + } ); + + it( 'should be an instance of BalloonPanelView', () => { + expect( balloon ).to.be.instanceOf( BalloonPanelView ); + } ); + + it( 'should host a badge view', () => { + expect( balloon.content.first ).to.be.instanceOf( View ); + } ); + + it( 'should have no arrow', () => { + expect( balloon.withArrow ).to.be.false; + } ); + + it( 'should not have a specific CSS class if not provided', () => { + expect( balloon.class ).to.be.undefined; + } ); + + it( 'should be added to editor\'s body view collection', () => { + expect( editor.ui.view.body.has( balloon ) ).to.be.true; + } ); + + it( 'should be registered in the focus tracker to avoid focus loss on click', () => { + sinon.assert.calledWith( focusTrackerAddSpy, balloon.element ); + } ); + } ); + + describe( 'badge view', () => { + let view; + + beforeEach( () => { + focusEditor( editor ); + + view = badge._balloonView.content.first; + } ); + + it( 'should have specific CSS classes', () => { + expect( view.element.classList.contains( 'ck-badge-extended' ) ).to.be.true; + } ); + + it( 'should have a label', () => { + expect( view.element.firstChild.tagName ).to.equal( 'SPAN' ); + expect( view.element.firstChild.classList.contains( 'ck-badge-extended__label' ) ).to.be.true; + expect( view.element.firstChild.textContent ).to.equal( 'Badge extended label' ); + } ); + + it( 'should not be accessible via tab key navigation', () => { + expect( view.element.firstChild.tabIndex ).to.equal( -1 ); + } ); + } ); + } ); + + describe( 'balloon positioning depending on environment and configuration', () => { + const originalGetVisible = Rect.prototype.getVisible; + let rootRect, balloonRect; + + beforeEach( () => { + rootRect = new Rect( { top: 0, left: 0, width: 400, right: 400, bottom: 100, height: 100 } ); + balloonRect = new Rect( { top: 0, left: 0, width: 20, right: 20, bottom: 10, height: 10 } ); + } ); + + it( 'should not show the balloon if the root is not visible vertically', async () => { + const domRoot = editor.editing.view.getDomRoot(); + const parentWithOverflow = document.createElement( 'div' ); + + parentWithOverflow.style.overflow = 'scroll'; + // Is not enough height to be visible vertically. + parentWithOverflow.style.height = '99px'; + + document.body.appendChild( parentWithOverflow ); + parentWithOverflow.appendChild( domRoot ); + + focusEditor( editor ); + + expect( badge._balloonView.isVisible ).to.be.true; + expect( badge._balloonView.position ).to.equal( 'arrowless' ); + + parentWithOverflow.remove(); + } ); + + it( 'should not show the balloon if the root is not visible horizontally', async () => { + const domRoot = editor.editing.view.getDomRoot(); + const parentWithOverflow = document.createElement( 'div' ); + + parentWithOverflow.style.overflow = 'scroll'; + // Is not enough width to be visible horizontally. + parentWithOverflow.style.width = '399px'; + + document.body.appendChild( parentWithOverflow ); + parentWithOverflow.appendChild( domRoot ); + + focusEditor( editor ); + + expect( badge._balloonView.isVisible ).to.be.true; + expect( badge._balloonView.position ).to.equal( 'arrowless' ); + + parentWithOverflow.remove(); + } ); + + it( 'should position to the left side if the UI language is RTL and no side was configured', async () => { + const editor = await createEditor( element, { + language: 'ar' + } ); + + badge = new BadgeExtended( editor ); + editor.fire( 'ready' ); + + testUtils.sinon.stub( editor.ui.getEditableElement( 'main' ), 'getBoundingClientRect' ).returns( { + top: 0, + left: 0, + right: 400, + width: 400, + bottom: 100, + height: 100 + } ); + + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( badge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( editor.editing.view.getDomRoot() ); + expect( positioningFunction( rootRect, balloonRect ) ).to.deep.equal( { + top: 95, + left: 5, + name: 'position_border-side_left', + config: { + withArrow: false + } + } ); + + await editor.destroy(); + } ); + + it( 'should position the balloon in the lower right corner by default', async () => { + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( badge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( editor.editing.view.getDomRoot() ); + expect( positioningFunction( rootRect, balloonRect ) ).to.deep.equal( { + top: 95, + left: 375, + name: 'position_border-side_right', + config: { + withArrow: false + } + } ); + } ); + + it( 'should hide the balloon if the root is invisible (cropped by ancestors)', async () => { + const editor = await createEditor( element ); + + badge = new BadgeExtended( editor ); + editor.fire( 'ready' ); + + const domRoot = editor.editing.view.getDomRoot(); + + rootRect = new Rect( { top: 0, left: 0, width: 100, right: 100, bottom: 10, height: 10 } ); + + testUtils.sinon.stub( rootRect, 'getVisible' ).returns( null ); + + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( badge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( domRoot ); + expect( positioningFunction( rootRect, balloonRect ) ).to.equal( null ); + + await editor.destroy(); + } ); + + it( 'should hide the balloon if displayed over the bottom root border but partially cropped by an ancestor', async () => { + const editor = await createEditor( element ); + + badge = new BadgeExtended( editor ); + editor.fire( 'ready' ); + + const domRoot = editor.editing.view.getDomRoot(); + + rootRect = new Rect( { top: 0, left: 0, width: 100, right: 100, bottom: 10, height: 10 } ); + + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( badge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( domRoot ); + expect( positioningFunction( rootRect, balloonRect ) ).to.equal( null ); + + await editor.destroy(); + } ); + + it( 'should not display the balloon if the root is narrower than 350px', async () => { + const domRoot = editor.editing.view.getDomRoot(); + + testUtils.sinon.stub( Rect.prototype, 'getVisible' ).callsFake( function() { + if ( this._source === domRoot ) { + return new Rect( domRoot ); + } else { + return originalGetVisible.call( this ); + } + } ); + + domRoot.getBoundingClientRect.returns( { + top: 0, + left: 0, + right: 349, + width: 349, + bottom: 100, + height: 100 + } ); + + focusEditor( editor ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + const pinSpy = testUtils.sinon.spy( badge._balloonView, 'pin' ); + + expect( badge._balloonView.isVisible ).to.be.true; + expect( badge._balloonView.position ).to.equal( 'arrowless' ); + + domRoot.getBoundingClientRect.returns( { + top: 0, + left: 0, + right: 350, + width: 350, + bottom: 100, + height: 100 + } ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + expect( badge._balloonView.isVisible ).to.be.true; + expect( badge._balloonView.position ).to.equal( 'position_border-side_right' ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( editor.editing.view.getDomRoot() ); + expect( positioningFunction( rootRect, balloonRect ) ).to.deep.equal( { + top: 95, + left: 325, + name: 'position_border-side_right', + config: { + withArrow: false + } + } ); + } ); + + it( 'should not display the balloon if the root is shorter than 50px', async () => { + const domRoot = editor.editing.view.getDomRoot(); + + testUtils.sinon.stub( Rect.prototype, 'getVisible' ).callsFake( function() { + if ( this._source === domRoot ) { + return new Rect( domRoot ); + } else { + return originalGetVisible.call( this ); + } + } ); + + domRoot.getBoundingClientRect.returns( { + top: 0, + left: 0, + right: 1000, + width: 1000, + bottom: 49, + height: 49 + } ); + + focusEditor( editor ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + const pinSpy = testUtils.sinon.spy( badge._balloonView, 'pin' ); + + expect( badge._balloonView.isVisible ).to.be.true; + expect( badge._balloonView.position ).to.equal( 'arrowless' ); + + domRoot.getBoundingClientRect.returns( { + top: 0, + left: 0, + right: 1000, + width: 1000, + bottom: 50, + height: 50 + } ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + expect( badge._balloonView.isVisible ).to.be.true; + expect( badge._balloonView.position ).to.equal( 'position_border-side_right' ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( editor.editing.view.getDomRoot() ); + expect( positioningFunction( rootRect, balloonRect ) ).to.deep.equal( { + top: 45, + left: 975, + name: 'position_border-side_right', + config: { + withArrow: false + } + } ); + } ); + } ); + + async function createEditor( element, config = { plugins: [ SourceEditing ] } ) { + return ClassicTestEditor.create( element, config ); + } + + function wait( time ) { + return new Promise( res => { + window.setTimeout( res, time ); + } ); + } + + function focusEditor( editor, focusableUIElement ) { + if ( !focusableUIElement ) { + focusableUIElement = editor.editing.view.getDomRoot(); + editor.editing.view.focus(); + } else { + focusableUIElement.focus(); + } + + editor.ui.focusTracker.focusedElement = focusableUIElement; + editor.ui.focusTracker.isFocused = true; + } + + function blurEditor( editor ) { + editor.ui.focusTracker.focusedElement = null; + editor.ui.focusTracker.isFocused = null; + } +} ); diff --git a/packages/ckeditor5-ui/tests/editorui/editorui.js b/packages/ckeditor5-ui/tests/editorui/editorui.js index 66beefbfe8a..6bc7c731fef 100644 --- a/packages/ckeditor5-ui/tests/editorui/editorui.js +++ b/packages/ckeditor5-ui/tests/editorui/editorui.js @@ -9,6 +9,7 @@ import ComponentFactory from '../../src/componentfactory.js'; import ToolbarView from '../../src/toolbar/toolbarview.js'; import TooltipManager from '../../src/tooltipmanager.js'; import PoweredBy from '../../src/editorui/poweredby.js'; +import EvaluationBadge from '../../src/editorui/evaluationbadge.js'; import AriaLiveAnnouncer from '../../src/arialiveannouncer.js'; import { EditorUIView, InlineEditableUIView, MenuBarView, View } from '../../src/index.js'; @@ -68,6 +69,10 @@ describe( 'EditorUI', () => { expect( ui.poweredBy ).to.be.instanceOf( PoweredBy ); } ); + it( 'should create #evaluationBadge', () => { + expect( ui.evaluationBadge ).to.be.instanceOf( EvaluationBadge ); + } ); + it( 'should create the aria live announcer instance', () => { expect( ui.ariaLiveAnnouncer ).to.be.instanceOf( AriaLiveAnnouncer ); } ); @@ -192,6 +197,14 @@ describe( 'EditorUI', () => { sinon.assert.calledOnce( destroySpy ); } ); + + it( 'should destroy #evaluationBadge', () => { + const destroySpy = sinon.spy( ui.evaluationBadge, 'destroy' ); + + ui.destroy(); + + sinon.assert.calledOnce( destroySpy ); + } ); } ); describe( 'setEditableElement()', () => { diff --git a/packages/ckeditor5-ui/tests/editorui/evaluationbadge.js b/packages/ckeditor5-ui/tests/editorui/evaluationbadge.js new file mode 100644 index 00000000000..bd00cd0ec78 --- /dev/null +++ b/packages/ckeditor5-ui/tests/editorui/evaluationbadge.js @@ -0,0 +1,1021 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* global document, window, HTMLElement, getComputedStyle, console */ + +import { Editor } from '@ckeditor/ckeditor5-core'; +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor.js'; +import EditorUI from '../../src/editorui/editorui.js'; +import { BalloonPanelView } from '../../src/index.js'; +import View from '../../src/view.js'; + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor.js'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils.js'; +import { Rect, global } from '@ckeditor/ckeditor5-utils'; +import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting.js'; +import Heading from '@ckeditor/ckeditor5-heading/src/heading.js'; +import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model.js'; +import generateKey from '@ckeditor/ckeditor5-core/tests/_utils/generatelicensekey.js'; + +describe( 'EvaluationBadge', () => { + let editor, element, developmentLicenseKey; + + testUtils.createSinonSandbox(); + + beforeEach( async () => { + sinon.stub( console, 'info' ); + developmentLicenseKey = generateKey( { licenseType: 'development' } ).licenseKey; + element = document.createElement( 'div' ); + document.body.appendChild( element ); + editor = await createEditor( element, { + plugins: [ SourceEditing ], + licenseKey: developmentLicenseKey + } ); + + testUtils.sinon.stub( editor.editing.view.getDomRoot(), 'getBoundingClientRect' ).returns( { + top: 0, + left: 0, + right: 400, + width: 400, + bottom: 100, + height: 100 + } ); + + testUtils.sinon.stub( document.body, 'getBoundingClientRect' ).returns( { + top: 0, + right: 1000, + bottom: 1000, + left: 0, + width: 1000, + height: 1000 + } ); + + sinon.stub( global.window, 'innerWidth' ).value( 1000 ); + sinon.stub( global.window, 'innerHeight' ).value( 1000 ); + } ); + + afterEach( async () => { + element.remove(); + await editor.destroy(); + } ); + + describe( 'constructor()', () => { + describe( 'balloon creation', () => { + it( 'should not throw if there is no view in EditorUI', () => { + expect( () => { + const editor = new Editor( { licenseKey: developmentLicenseKey } ); + + editor.model.document.createRoot(); + editor.ui = new EditorUI( editor ); + editor.editing.view.attachDomRoot( element ); + editor.fire( 'ready' ); + element.style.display = 'block'; + element.setAttribute( 'contenteditable', 'true' ); + editor.ui.focusTracker.add( element ); + element.focus(); + + editor.destroy(); + editor.ui.destroy(); + } ).to.not.throw(); + } ); + + it( 'should create the balloon on demand', () => { + expect( editor.ui.evaluationBadge._balloonView ).to.be.null; + + focusEditor( editor ); + + expect( editor.ui.evaluationBadge._balloonView ).to.be.instanceOf( BalloonPanelView ); + } ); + + it( 'should create the balloon when license type is `evaluation`', async () => { + const { licenseKey, todayTimestamp } = generateKey( { + licenseType: 'evaluation', + isExpired: false, + daysAfterExpiration: -1 + } ); + + const today = todayTimestamp; + const dateNow = sinon.stub( Date, 'now' ).returns( today ); + + const editor = await createEditor( element, { + licenseKey + } ); + + expect( editor.ui.evaluationBadge._balloonView ).to.be.null; + + focusEditor( editor ); + + expect( editor.ui.evaluationBadge._balloonView ).to.be.instanceOf( BalloonPanelView ); + + const balloonElement = editor.ui.evaluationBadge._balloonView.element; + + expect( balloonElement.querySelector( '.ck-evaluation-badge__label' ).textContent ).to.equal( + 'For evaluation purposes only' + ); + + await editor.destroy(); + + dateNow.restore(); + } ); + + it( 'should create the balloon when license type is `trial`', async () => { + const { licenseKey, todayTimestamp } = generateKey( { + licenseType: 'trial', + isExpired: false, + daysAfterExpiration: -1 + } ); + + const today = todayTimestamp; + const dateNow = sinon.stub( Date, 'now' ).returns( today ); + + const editor = await createEditor( element, { + licenseKey + } ); + + expect( editor.ui.evaluationBadge._balloonView ).to.be.null; + + focusEditor( editor ); + + expect( editor.ui.evaluationBadge._balloonView ).to.be.instanceOf( BalloonPanelView ); + + const balloonElement = editor.ui.evaluationBadge._balloonView.element; + + expect( balloonElement.querySelector( '.ck-evaluation-badge__label' ).textContent ).to.equal( + 'For evaluation purposes only' + ); + + await editor.destroy(); + + dateNow.restore(); + } ); + + it( 'should create the balloon when license type is `development`', async () => { + const editor = await createEditor( element, { + licenseKey: developmentLicenseKey + } ); + + expect( editor.ui.evaluationBadge._balloonView ).to.be.null; + + focusEditor( editor ); + + expect( editor.ui.evaluationBadge._balloonView ).to.be.instanceOf( BalloonPanelView ); + + const balloonElement = editor.ui.evaluationBadge._balloonView.element; + + expect( balloonElement.querySelector( '.ck-evaluation-badge__label' ).textContent ).to.equal( + 'For development purposes only' + ); + + await editor.destroy(); + } ); + + it( 'should not depend on white-label', async () => { + const { licenseKey } = generateKey( { whiteLabel: true, licenseType: 'development' } ); + const editor = await createEditor( element, { + licenseKey + } ); + + expect( editor.ui.evaluationBadge._balloonView ).to.be.null; + + focusEditor( editor ); + + expect( editor.ui.evaluationBadge._balloonView ).to.be.instanceOf( BalloonPanelView ); + + await editor.destroy(); + } ); + } ); + + describe( 'balloon management on editor focus change', () => { + const originalGetVisible = Rect.prototype.getVisible; + + it( 'should show the balloon when the editor gets focused', () => { + focusEditor( editor ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.true; + } ); + + it( 'should show the balloon if the focus is not in the editing root but in other editor UI', async () => { + const focusableEditorUIElement = document.createElement( 'input' ); + focusableEditorUIElement.type = 'text'; + document.body.appendChild( focusableEditorUIElement ); + + editor.ui.focusTracker.add( focusableEditorUIElement ); + + // Just generate the balloon on demand. + focusEditor( editor ); + blurEditor( editor ); + + await wait( 10 ); + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + focusEditor( editor, focusableEditorUIElement ); + + sinon.assert.calledOnce( pinSpy ); + sinon.assert.calledWith( pinSpy, sinon.match.has( 'target', editor.editing.view.getDomRoot() ) ); + + focusableEditorUIElement.remove(); + } ); + + it( 'should hide the balloon on blur', async () => { + focusEditor( editor ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.true; + + blurEditor( editor ); + + // FocusTracker's blur handler is asynchronous. + await wait( 200 ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.false; + } ); + + // This is a weak test because it does not check the geometry but it will do. + it( 'should show the balloon when the source editing is engaged', async () => { + const domRoot = editor.editing.view.getDomRoot(); + const originalGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect; + + function isEditableElement( element ) { + return Array.from( editor.ui.getEditableElementsNames() ).map( name => { + return editor.ui.getEditableElement( name ); + } ).includes( element ); + } + + // Rect#getVisible() passthrough to ignore ancestors. Makes testing a lot easier. + testUtils.sinon.stub( Rect.prototype, 'getVisible' ).callsFake( function() { + if ( isEditableElement( this._source ) ) { + return new Rect( this._source ); + } else { + return originalGetVisible.call( this ); + } + } ); + + // Stub textarea's client rect. + testUtils.sinon.stub( HTMLElement.prototype, 'getBoundingClientRect' ).callsFake( function() { + if ( this.parentNode.classList.contains( 'ck-source-editing-area' ) ) { + return { + top: 0, + left: 0, + right: 400, + width: 400, + bottom: 200, + height: 200 + }; + } + + return originalGetBoundingClientRect.call( this ); + } ); + + focusEditor( editor ); + + domRoot.getBoundingClientRect.returns( { + top: 0, + left: 0, + right: 350, + width: 350, + bottom: 100, + height: 100 + } ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + await wait( 75 ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.true; + expect( editor.ui.evaluationBadge._balloonView.position ).to.equal( 'position_border-side_left' ); + sinon.assert.calledWith( pinSpy.lastCall, sinon.match.has( 'target', domRoot ) ); + + editor.plugins.get( 'SourceEditing' ).isSourceEditingMode = true; + + const sourceAreaElement = editor.ui.getEditableElement( 'sourceEditing:main' ); + + focusEditor( editor, sourceAreaElement ); + sinon.assert.calledWith( + pinSpy.lastCall, + sinon.match.has( 'target', sourceAreaElement ) + ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.true; + expect( editor.ui.evaluationBadge._balloonView.position ).to.equal( 'position_border-side_left' ); + + editor.plugins.get( 'SourceEditing' ).isSourceEditingMode = false; + focusEditor( editor ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.true; + expect( editor.ui.evaluationBadge._balloonView.position ).to.equal( 'position_border-side_left' ); + sinon.assert.calledWith( pinSpy.lastCall, sinon.match.has( 'target', domRoot ) ); + } ); + } ); + + describe( 'balloon management on EditorUI#update', () => { + it( 'should not trigger if the editor is not focused', () => { + expect( editor.ui.evaluationBadge._balloonView ).to.be.null; + + editor.ui.fire( 'update' ); + + expect( editor.ui.evaluationBadge._balloonView ).to.be.null; + } ); + + it( 'should (re-)show the balloon but throttled', async () => { + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + editor.ui.fire( 'update' ); + + sinon.assert.notCalled( pinSpy ); + + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + sinon.assert.calledWith( pinSpy.firstCall, sinon.match.has( 'target', editor.editing.view.getDomRoot() ) ); + } ); + + it( 'should (re-)show the balloon if the focus is not in the editing root but in other editor UI', async () => { + const focusableEditorUIElement = document.createElement( 'input' ); + focusableEditorUIElement.type = 'text'; + editor.ui.focusTracker.add( focusableEditorUIElement ); + document.body.appendChild( focusableEditorUIElement ); + + focusEditor( editor, focusableEditorUIElement ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + sinon.assert.notCalled( pinSpy ); + + editor.ui.fire( 'update' ); + editor.ui.fire( 'update' ); + + sinon.assert.calledOnce( pinSpy ); + + await wait( 75 ); + + sinon.assert.calledTwice( pinSpy ); + sinon.assert.calledWith( pinSpy, sinon.match.has( 'target', editor.editing.view.getDomRoot() ) ); + focusableEditorUIElement.remove(); + } ); + } ); + + describe( 'balloon view', () => { + let balloon, focusTrackerAddSpy; + + beforeEach( () => { + focusTrackerAddSpy = testUtils.sinon.spy( editor.ui.focusTracker, 'add' ); + + focusEditor( editor ); + + balloon = editor.ui.evaluationBadge._balloonView; + } ); + + it( 'should be an instance of BalloonPanelView', () => { + expect( balloon ).to.be.instanceOf( BalloonPanelView ); + } ); + + it( 'should host an evaluation badge view', () => { + expect( balloon.content.first ).to.be.instanceOf( View ); + } ); + + it( 'should have no arrow', () => { + expect( balloon.withArrow ).to.be.false; + } ); + + it( 'should have a specific CSS class', () => { + expect( balloon.class ).to.equal( 'ck-evaluation-badge-balloon' ); + } ); + + it( 'should be added to editor\'s body view collection', () => { + expect( editor.ui.view.body.has( balloon ) ).to.be.true; + } ); + + it( 'should be registered in the focus tracker to avoid focus loss on click', () => { + sinon.assert.calledWith( focusTrackerAddSpy, balloon.element ); + } ); + } ); + + describe( 'evaluation badge view', () => { + let view; + + beforeEach( () => { + focusEditor( editor ); + + view = editor.ui.evaluationBadge._balloonView.content.first; + } ); + + it( 'should have specific CSS classes', () => { + expect( view.element.classList.contains( 'ck' ) ).to.be.true; + expect( view.element.classList.contains( 'ck-evaluation-badge' ) ).to.be.true; + } ); + + it( 'should be excluded from the accessibility tree', () => { + expect( view.element.getAttribute( 'aria-hidden' ) ).to.equal( 'true' ); + } ); + + it( 'should not be accessible via tab key navigation', () => { + expect( view.element.firstChild.tabIndex ).to.equal( -1 ); + } ); + } ); + } ); + + describe( 'destroy()', () => { + describe( 'if there was a balloon', () => { + beforeEach( () => { + focusEditor( editor ); + } ); + + it( 'should unpin the balloon', () => { + const unpinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'unpin' ); + + editor.destroy(); + + sinon.assert.calledOnce( unpinSpy ); + } ); + + it( 'should destroy the balloon', () => { + const destroySpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'destroy' ); + + editor.destroy(); + + sinon.assert.called( destroySpy ); + + expect( editor.ui.evaluationBadge._balloonView ).to.be.null; + } ); + + it( 'should cancel any throttled show to avoid post-destroy timed errors', () => { + const spy = testUtils.sinon.spy( editor.ui.evaluationBadge._showBalloonThrottled, 'cancel' ); + + editor.destroy(); + + sinon.assert.calledOnce( spy ); + } ); + } ); + + describe( 'if there was no balloon', () => { + it( 'should not throw', () => { + expect( () => { + editor.destroy(); + } ).to.not.throw(); + } ); + } ); + + it( 'should destroy the emitter listeners', () => { + const spy = testUtils.sinon.spy( editor.ui.evaluationBadge, 'stopListening' ); + + editor.destroy(); + + sinon.assert.calledOnce( spy ); + } ); + } ); + + describe( 'balloon positioning depending on environment and configuration', () => { + const originalGetVisible = Rect.prototype.getVisible; + let rootRect, balloonRect; + + beforeEach( () => { + rootRect = new Rect( { top: 0, left: 0, width: 400, right: 400, bottom: 100, height: 100 } ); + balloonRect = new Rect( { top: 0, left: 0, width: 20, right: 20, bottom: 10, height: 10 } ); + } ); + + it( 'should not show the balloon if the root is not visible vertically', async () => { + const domRoot = editor.editing.view.getDomRoot(); + const parentWithOverflow = document.createElement( 'div' ); + + parentWithOverflow.style.overflow = 'scroll'; + // Is not enough height to be visible vertically. + parentWithOverflow.style.height = '99px'; + + document.body.appendChild( parentWithOverflow ); + parentWithOverflow.appendChild( domRoot ); + + focusEditor( editor ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.true; + expect( editor.ui.evaluationBadge._balloonView.position ).to.equal( 'arrowless' ); + + parentWithOverflow.remove(); + } ); + + it( 'should not show the balloon if the root is not visible horizontally', async () => { + const domRoot = editor.editing.view.getDomRoot(); + const parentWithOverflow = document.createElement( 'div' ); + + parentWithOverflow.style.overflow = 'scroll'; + // Is not enough width to be visible horizontally. + parentWithOverflow.style.width = '399px'; + + document.body.appendChild( parentWithOverflow ); + parentWithOverflow.appendChild( domRoot ); + + focusEditor( editor ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.true; + expect( editor.ui.evaluationBadge._balloonView.position ).to.equal( 'arrowless' ); + + parentWithOverflow.remove(); + } ); + + it( 'should position the badge to the left right if the UI language is RTL (and powered-by is on the left)', async () => { + const editor = await createEditor( element, { + language: 'ar', + licenseKey: developmentLicenseKey + } ); + + testUtils.sinon.stub( editor.ui.getEditableElement( 'main' ), 'getBoundingClientRect' ).returns( { + top: 0, + left: 0, + right: 400, + width: 400, + bottom: 100, + height: 100 + } ); + + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( editor.editing.view.getDomRoot() ); + expect( positioningFunction( rootRect, balloonRect ) ).to.deep.equal( { + top: 95, + left: 375, + name: 'position_border-side_right', + config: { + withArrow: false + } + } ); + + await editor.destroy(); + } ); + + it( 'should position the balloon in the lower left corner by default', async () => { + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( editor.editing.view.getDomRoot() ); + expect( positioningFunction( rootRect, balloonRect ) ).to.deep.equal( { + top: 95, + left: 5, + name: 'position_border-side_left', + config: { + withArrow: false + } + } ); + } ); + + it( 'should position the balloon in the lower right corner if poweredby is configured on the left', async () => { + const editor = await createEditor( element, { + ui: { + poweredBy: { + side: 'left' + } + }, + licenseKey: developmentLicenseKey + } ); + + testUtils.sinon.stub( editor.ui.getEditableElement( 'main' ), 'getBoundingClientRect' ).returns( { + top: 0, + left: 0, + right: 400, + width: 400, + bottom: 100, + height: 100 + } ); + + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( editor.editing.view.getDomRoot() ); + expect( positioningFunction( rootRect, balloonRect ) ).to.deep.equal( { + top: 95, + left: 375, + name: 'position_border-side_right', + config: { + withArrow: false + } + } ); + + await editor.destroy(); + } ); + + it( 'should position the balloon over the bottom root border if configured', async () => { + const editor = await createEditor( element, { + ui: { + poweredBy: { + position: 'border' + } + }, + licenseKey: developmentLicenseKey + } ); + + testUtils.sinon.stub( editor.ui.getEditableElement( 'main' ), 'getBoundingClientRect' ).returns( { + top: 0, + left: 0, + right: 400, + width: 400, + bottom: 100, + height: 100 + } ); + + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( editor.editing.view.getDomRoot() ); + expect( positioningFunction( rootRect, balloonRect ) ).to.deep.equal( { + top: 95, + left: 5, + name: 'position_border-side_left', + config: { + withArrow: false + } + } ); + + await editor.destroy(); + } ); + + it( 'should position the balloon in the corner of the root if configured', async () => { + const editor = await createEditor( element, { + ui: { + poweredBy: { + position: 'inside' + } + }, + licenseKey: developmentLicenseKey + } ); + + testUtils.sinon.stub( editor.ui.getEditableElement( 'main' ), 'getBoundingClientRect' ).returns( { + top: 0, + left: 0, + right: 400, + width: 400, + bottom: 100, + height: 100 + } ); + + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( editor.editing.view.getDomRoot() ); + expect( positioningFunction( rootRect, balloonRect ) ).to.deep.equal( { + top: 90, + left: 5, + name: 'position_inside-side_left', + config: { + withArrow: false + } + } ); + + await editor.destroy(); + } ); + + it( 'should hide the balloon if displayed over the bottom root border but partially cropped by an ancestor', async () => { + const editor = await createEditor( element, { + ui: { + poweredBy: { + position: 'border' + } + }, + licenseKey: developmentLicenseKey + } ); + + const domRoot = editor.editing.view.getDomRoot(); + + rootRect = new Rect( { top: 0, left: 0, width: 100, right: 100, bottom: 10, height: 10 } ); + + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( domRoot ); + expect( positioningFunction( rootRect, balloonRect ) ).to.equal( null ); + + await editor.destroy(); + } ); + + it( 'should hide the balloon if displayed in the corner of the root but partially cropped by an ancestor', async () => { + const editor = await createEditor( element, { + ui: { + poweredBy: { + position: 'inside' + } + }, + licenseKey: developmentLicenseKey + } ); + + rootRect = new Rect( { top: 0, left: 0, width: 400, right: 400, bottom: 200, height: 200 } ); + + testUtils.sinon.stub( rootRect, 'getVisible' ).returns( { top: 0, left: 0, width: 400, right: 400, bottom: 10, height: 10 } ); + + balloonRect = new Rect( { top: 200, left: 0, width: 20, right: 20, bottom: 210, height: 10 } ); + + const domRoot = editor.editing.view.getDomRoot(); + + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( domRoot ); + expect( positioningFunction( rootRect, balloonRect ) ).to.equal( null ); + + await editor.destroy(); + } ); + + it( 'should not display the balloon if the root is narrower than 350px', async () => { + const domRoot = editor.editing.view.getDomRoot(); + + testUtils.sinon.stub( Rect.prototype, 'getVisible' ).callsFake( function() { + if ( this._source === domRoot ) { + return new Rect( domRoot ); + } else { + return originalGetVisible.call( this ); + } + } ); + + domRoot.getBoundingClientRect.returns( { + top: 0, + left: 0, + right: 349, + width: 349, + bottom: 100, + height: 100 + } ); + + focusEditor( editor ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.true; + expect( editor.ui.evaluationBadge._balloonView.position ).to.equal( 'arrowless' ); + + domRoot.getBoundingClientRect.returns( { + top: 0, + left: 0, + right: 350, + width: 350, + bottom: 100, + height: 100 + } ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.true; + expect( editor.ui.evaluationBadge._balloonView.position ).to.equal( 'position_border-side_left' ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( editor.editing.view.getDomRoot() ); + expect( positioningFunction( rootRect, balloonRect ) ).to.deep.equal( { + top: 95, + left: 5, + name: 'position_border-side_left', + config: { + withArrow: false + } + } ); + } ); + + it( 'should not display the balloon if the root is shorter than 50px', async () => { + const domRoot = editor.editing.view.getDomRoot(); + + testUtils.sinon.stub( Rect.prototype, 'getVisible' ).callsFake( function() { + if ( this._source === domRoot ) { + return new Rect( domRoot ); + } else { + return originalGetVisible.call( this ); + } + } ); + + domRoot.getBoundingClientRect.returns( { + top: 0, + left: 0, + right: 1000, + width: 1000, + bottom: 49, + height: 49 + } ); + + focusEditor( editor ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.true; + expect( editor.ui.evaluationBadge._balloonView.position ).to.equal( 'arrowless' ); + + domRoot.getBoundingClientRect.returns( { + top: 0, + left: 0, + right: 1000, + width: 1000, + bottom: 50, + height: 50 + } ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.true; + expect( editor.ui.evaluationBadge._balloonView.position ).to.equal( 'position_border-side_left' ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( editor.editing.view.getDomRoot() ); + expect( positioningFunction( rootRect, balloonRect ) ).to.deep.equal( { + top: 45, + left: 5, + name: 'position_border-side_left', + config: { + withArrow: false + } + } ); + } ); + } ); + + it( 'should have the z-index lower than a regular BalloonPanelView instance', () => { + focusEditor( editor ); + + const balloonView = new BalloonPanelView(); + balloonView.render(); + + const zIndexOfEvaluationBadgeBalloon = Number( getComputedStyle( editor.ui.evaluationBadge._balloonView.element ).zIndex ); + + document.body.appendChild( balloonView.element ); + + const zIndexOfRegularBalloon = Number( getComputedStyle( balloonView.element ).zIndex ); + + expect( zIndexOfEvaluationBadgeBalloon ).to.be.lessThan( zIndexOfRegularBalloon ); + + balloonView.element.remove(); + balloonView.destroy(); + } ); + + it( 'should not overlap a dropdown panel in a toolbar', async () => { + const editor = await createClassicEditor( element, { + toolbar: [ 'heading' ], + plugins: [ Heading ], + ui: { + poweredBy: { + position: 'inside' + } + }, + licenseKey: developmentLicenseKey + } ); + + setData( editor.model, 'foo[]bar' ); + + focusEditor( editor ); + + const headingToolbarButton = editor.ui.view.toolbar.items + .find( item => item.buttonView && item.buttonView.label.startsWith( 'Heading' ) ); + + const evaluationBadgeElement = editor.ui.evaluationBadge._balloonView.element; + + const evaluationBadgeElementGeometry = new Rect( evaluationBadgeElement ); + + const middleOfTheEvaluationBadgeCoords = { + x: ( evaluationBadgeElementGeometry.width / 2 ) + evaluationBadgeElementGeometry.left, + y: ( evaluationBadgeElementGeometry.height / 2 ) + evaluationBadgeElementGeometry.top + }; + + let elementFromPoint = document.elementFromPoint( + middleOfTheEvaluationBadgeCoords.x, + middleOfTheEvaluationBadgeCoords.y + ); + + expect( elementFromPoint.classList.contains( 'ck-evaluation-badge__label' ) ).to.be.true; + + // show heading dropdown + headingToolbarButton.buttonView.fire( 'execute' ); + + elementFromPoint = document.elementFromPoint( + middleOfTheEvaluationBadgeCoords.x, + middleOfTheEvaluationBadgeCoords.y + ); + + expect( elementFromPoint.classList.contains( 'ck-button__label' ) ).to.be.true; + + await editor.destroy(); + } ); + + async function createEditor( element, config = {} ) { + return ClassicTestEditor.create( element, config ); + } + + async function createClassicEditor( element, config = {} ) { + return ClassicEditor.create( element, config ); + } + + function wait( time ) { + return new Promise( res => { + window.setTimeout( res, time ); + } ); + } + + function focusEditor( editor, focusableUIElement ) { + if ( !focusableUIElement ) { + focusableUIElement = editor.editing.view.getDomRoot(); + editor.editing.view.focus(); + } else { + focusableUIElement.focus(); + } + + editor.ui.focusTracker.focusedElement = focusableUIElement; + editor.ui.focusTracker.isFocused = true; + } + + function blurEditor( editor ) { + editor.ui.focusTracker.focusedElement = null; + editor.ui.focusTracker.isFocused = null; + } +} ); diff --git a/packages/ckeditor5-ui/tests/editorui/poweredby.js b/packages/ckeditor5-ui/tests/editorui/poweredby.js index fdcee1ccae4..253407483d2 100644 --- a/packages/ckeditor5-ui/tests/editorui/poweredby.js +++ b/packages/ckeditor5-ui/tests/editorui/poweredby.js @@ -7,16 +7,17 @@ import { Editor } from '@ckeditor/ckeditor5-core'; import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor.js'; -import EditorUI from '../../src/editorui/editorui.js'; -import { BalloonPanelView } from '../../src/index.js'; -import View from '../../src/view.js'; - import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor.js'; -import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils.js'; -import { Rect, global } from '@ckeditor/ckeditor5-utils'; import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting.js'; import Heading from '@ckeditor/ckeditor5-heading/src/heading.js'; +import { Rect, global } from '@ckeditor/ckeditor5-utils'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils.js'; import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model.js'; +import generateKey from '@ckeditor/ckeditor5-core/tests/_utils/generatelicensekey.js'; + +import EditorUI from '../../src/editorui/editorui.js'; +import { BalloonPanelView } from '../../src/index.js'; +import View from '../../src/view.js'; describe( 'PoweredBy', () => { let editor, element; @@ -83,12 +84,42 @@ describe( 'PoweredBy', () => { expect( editor.ui.poweredBy._balloonView ).to.be.instanceOf( BalloonPanelView ); } ); - it( 'should not create the balloon when a valid license key is configured', async () => { + it( 'should create the balloon when license is `GPL`', async () => { + const editor = await createEditor( element, { + licenseKey: 'GPL' + } ); + + expect( editor.ui.poweredBy._balloonView ).to.be.null; + + focusEditor( editor ); + + expect( editor.ui.poweredBy._balloonView ).to.be.instanceOf( BalloonPanelView ); + + await editor.destroy(); + } ); + + it( 'should create the balloon when license is invalid', async () => { + const showErrorStub = sinon.stub( ClassicTestEditor.prototype, '_showLicenseError' ); + + const editor = await createEditor( element, { + licenseKey: '' + } ); + + expect( editor.ui.poweredBy._balloonView ).to.be.null; + + focusEditor( editor ); + + expect( editor.ui.poweredBy._balloonView ).to.be.instanceOf( BalloonPanelView ); + + await editor.destroy(); + + showErrorStub.restore(); + } ); + + it( 'should not create the balloon when a white-label license key is configured', async () => { + const { licenseKey } = generateKey( { whiteLabel: true } ); const editor = await createEditor( element, { - // eslint-disable-next-line max-len - // https://github.com/ckeditor/ckeditor5/blob/226bf243d1eb8bae2d447f631d6f5d9961bc6541/packages/ckeditor5-utils/tests/verifylicense.js#L14 - // eslint-disable-next-line max-len - licenseKey: 'dG9vZWFzZXRtcHNsaXVyb3JsbWlkbXRvb2Vhc2V0bXBzbGl1cm9ybG1pZG10b29lYXNldG1wc2xpdXJvcmxtaWRtLU1qQTBOREEyTVRJPQ==' + licenseKey } ); expect( editor.ui.poweredBy._balloonView ).to.be.null; @@ -100,12 +131,10 @@ describe( 'PoweredBy', () => { await editor.destroy(); } ); - it( 'should create the balloon when a valid license key is configured and `forceVisible` is set to true', async () => { + it( 'should create the balloon when a white-label license key is configured and `forceVisible` is set to true', async () => { + const { licenseKey } = generateKey( { whiteLabel: true } ); const editor = await createEditor( element, { - // eslint-disable-next-line max-len - // https://github.com/ckeditor/ckeditor5/blob/226bf243d1eb8bae2d447f631d6f5d9961bc6541/packages/ckeditor5-utils/tests/verifylicense.js#L14 - // eslint-disable-next-line max-len - licenseKey: 'dG9vZWFzZXRtcHNsaXVyb3JsbWlkbXRvb2Vhc2V0bXBzbGl1cm9ybG1pZG10b29lYXNldG1wc2xpdXJvcmxtaWRtLU1qQTBOREEyTVRJPQ==', + licenseKey, ui: { poweredBy: { forceVisible: true @@ -121,6 +150,21 @@ describe( 'PoweredBy', () => { await editor.destroy(); } ); + + it( 'should create the balloon when a non-white-label license key is configured', async () => { + const { licenseKey } = generateKey(); + const editor = await createEditor( element, { + licenseKey + } ); + + expect( editor.ui.poweredBy._balloonView ).to.be.null; + + focusEditor( editor ); + + expect( editor.ui.poweredBy._balloonView ).to.be.instanceOf( BalloonPanelView ); + + await editor.destroy(); + } ); } ); describe( 'balloon management on editor focus change', () => { diff --git a/packages/ckeditor5-ui/theme/globals/_evaluationbadge.css b/packages/ckeditor5-ui/theme/globals/_evaluationbadge.css new file mode 100644 index 00000000000..f0f18bd592a --- /dev/null +++ b/packages/ckeditor5-ui/theme/globals/_evaluationbadge.css @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +:root { + --ck-evaluation-badge-font-size: 7.5px; + --ck-evaluation-badge-line-height: 7.5px; + --ck-evaluation-badge-padding-vertical: 2px; + --ck-evaluation-badge-padding-horizontal: 4px; + --ck-evaluation-badge-text-color: hsl(0, 0%, 31%); + --ck-evaluation-badge-border-radius: var(--ck-border-radius); + --ck-evaluation-badge-background: hsl(0, 0%, 100%); + --ck-evaluation-badge-border-color: var(--ck-color-focus-border); +} + +.ck.ck-balloon-panel.ck-evaluation-badge-balloon { + --ck-border-radius: var(--ck-evaluation-badge-border-radius); + + box-shadow: none; + background: var(--ck-evaluation-badge-background); + min-height: unset; + z-index: calc( var(--ck-z-panel) - 1 ); + + & .ck.ck-evaluation-badge { + line-height: var(--ck-evaluation-badge-line-height); + padding: var(--ck-evaluation-badge-padding-vertical) var(--ck-evaluation-badge-padding-horizontal); + + & .ck-evaluation-badge__label { + font-size: var(--ck-evaluation-badge-font-size); + letter-spacing: -.2px; + padding-left: 2px; + text-transform: uppercase; + font-weight: bold; + margin-right: 4px; + line-height: normal; + color: var(--ck-evaluation-badge-text-color); + } + } + + &[class*="position_inside"] { + border-color: transparent; + } + + &[class*="position_border"] { + border: var(--ck-focus-ring); + border-color: var(--ck-evaluation-badge-border-color); + } +} + diff --git a/packages/ckeditor5-ui/theme/globals/globals.css b/packages/ckeditor5-ui/theme/globals/globals.css index 68eaa1bef2a..01e9921a73f 100644 --- a/packages/ckeditor5-ui/theme/globals/globals.css +++ b/packages/ckeditor5-ui/theme/globals/globals.css @@ -7,3 +7,4 @@ @import "./_zindex.css"; @import "./_transition.css"; @import "./_poweredby.css"; +@import "./_evaluationbadge.css"; diff --git a/packages/ckeditor5-undo/docs/_snippets/features/undo-redo.js b/packages/ckeditor5-undo/docs/_snippets/features/undo-redo.js index f244ead6033..1ffbd8895ca 100644 --- a/packages/ckeditor5-undo/docs/_snippets/features/undo-redo.js +++ b/packages/ckeditor5-undo/docs/_snippets/features/undo-redo.js @@ -50,7 +50,8 @@ ClassicEditor }, table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-undo/docs/features/undo-redo.md b/packages/ckeditor5-undo/docs/features/undo-redo.md index 8ca43f04958..85202071e05 100644 --- a/packages/ckeditor5-undo/docs/features/undo-redo.md +++ b/packages/ckeditor5-undo/docs/features/undo-redo.md @@ -36,17 +36,15 @@ The feature supports both toolbar buttons and {@link features/accessibility#keyb Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, Undo } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { - // Load the plugin. + licenseKey: '', // Or 'GPL'. plugins: [ Undo, /* ... */ ], - - // Display the "Undo" and "Redo" buttons in the toolbar. toolbar: [ 'undo', 'redo', /* ... */ ], } ) .then( /* ... */ ) diff --git a/packages/ckeditor5-upload/docs/_snippets/features/base64-upload.js b/packages/ckeditor5-upload/docs/_snippets/features/base64-upload.js index f3596b82d21..07384c21594 100644 --- a/packages/ckeditor5-upload/docs/_snippets/features/base64-upload.js +++ b/packages/ckeditor5-upload/docs/_snippets/features/base64-upload.js @@ -18,7 +18,8 @@ ClassicEditor viewportOffset: { top: window.getViewportTopOffsetConfig() } - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-upload/docs/features/base64-upload-adapter.md b/packages/ckeditor5-upload/docs/features/base64-upload-adapter.md index 4d1d80774af..fb8644bffdc 100644 --- a/packages/ckeditor5-upload/docs/features/base64-upload-adapter.md +++ b/packages/ckeditor5-upload/docs/features/base64-upload-adapter.md @@ -35,13 +35,14 @@ Use the editor below to see the adapter in action. Open the web browser console Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, Base64UploadAdapter } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Base64UploadAdapter, /* ... */ ], toolbar: [ /* ... */ ] } ) diff --git a/packages/ckeditor5-upload/docs/features/simple-upload-adapter.md b/packages/ckeditor5-upload/docs/features/simple-upload-adapter.md index 9a346020376..d4212c9e914 100644 --- a/packages/ckeditor5-upload/docs/features/simple-upload-adapter.md +++ b/packages/ckeditor5-upload/docs/features/simple-upload-adapter.md @@ -17,17 +17,18 @@ The simple upload adapter lets you upload images to your server using the [`XMLH Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, SimpleUploadAdapter } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ SimpleUploadAdapter, /* ... */ ], toolbar: [ /* ... */ ], simpleUpload: { - // Feature configuration. + // Configuration. } } ) .then( /* ... */ ) @@ -39,12 +40,9 @@ ClassicEditor The client side of this feature is configurable using the {@link module:upload/uploadconfig~SimpleUploadConfig `config.simpleUpload`} object. ```js -import { ClassicEditor, SimpleUploadAdapter } from 'ckeditor5'; - ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ SimpleUploadAdapter, /* ... */ ], - toolbar: [ /* ... */ ], + // ... Other configuration options ... simpleUpload: { // The URL that the images are uploaded to. uploadUrl: 'http://example.com', diff --git a/packages/ckeditor5-utils/src/crc32.ts b/packages/ckeditor5-utils/src/crc32.ts new file mode 100644 index 00000000000..4734b638a5d --- /dev/null +++ b/packages/ckeditor5-utils/src/crc32.ts @@ -0,0 +1,80 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module utils/crc32 + */ + +/** + * Generates a CRC lookup table. + * This function creates and returns a 256-element array of pre-computed CRC values for quick CRC calculation. + * It uses the polynomial 0xEDB88320 to compute each value in the loop, optimizing future CRC calculations. + */ +function makeCrcTable(): Array { + const crcTable: Array = []; + + for ( let n = 0; n < 256; n++ ) { + let c: number = n; + + for ( let k = 0; k < 8; k++ ) { + if ( c & 1 ) { + c = 0xEDB88320 ^ ( c >>> 1 ); + } else { + c = c >>> 1; + } + } + + crcTable[ n ] = c; + } + + return crcTable; +} + +/** + * Calculates CRC-32 checksum for a given inputData to verify the integrity of data. + * + * @param inputData Accepts a single value (string, number, boolean), an array of strings, or an array of all of the above types. + * Non-string values are converted to strings before calculating the checksum. + * The checksum calculation is based on the concatenated string representation of the input values: + * * `crc32('foo')` is equivalent to `crc32(['foo'])` + * * `crc32(123)` is equivalent to `crc32(['123'])` + * * `crc32(true)` is equivalent to `crc32(['true'])` + * * `crc32(['foo', 123, true])` produces the same result as `crc32('foo123true')` + * * Nested arrays of strings are flattened, so `crc32([['foo', 'bar'], 'baz'])` is equivalent to `crc32(['foobar', 'baz'])` + * + * @returns The CRC-32 checksum, returned as a hexadecimal string. + */ +export default function crc32( inputData: CRCData ): string { + const dataArray = Array.isArray( inputData ) ? inputData : [ inputData ]; + const crcTable: Array = makeCrcTable(); + let crc: number = 0 ^ ( -1 ); + + // Convert data to a single string. + const dataString: string = dataArray.map( item => { + if ( Array.isArray( item ) ) { + return item.join( '' ); + } + + return String( item ); + } ).join( '' ); + + // Calculate the CRC for the resulting string. + for ( let i = 0; i < dataString.length; i++ ) { + const byte: number = dataString.charCodeAt( i ); + crc = ( crc >>> 8 ) ^ crcTable[ ( crc ^ byte ) & 0xFF ]; + } + + crc = ( crc ^ ( -1 ) ) >>> 0; // Force unsigned integer. + + return crc.toString( 16 ).padStart( 8, '0' ); +} + +/** + * The input data for the CRC-32 checksum calculation. + * Can be a single value (string, number, boolean), an array of strings, or an array of all of the above types. + */ +export type CRCData = CRCValue | Array; + +type CRCValue = string | number | boolean | Array; diff --git a/packages/ckeditor5-utils/src/index.ts b/packages/ckeditor5-utils/src/index.ts index 0cc02b52f64..f6a32c94b1f 100644 --- a/packages/ckeditor5-utils/src/index.ts +++ b/packages/ckeditor5-utils/src/index.ts @@ -91,8 +91,9 @@ export { default as spliceArray } from './splicearray.js'; export { default as uid } from './uid.js'; export { default as delay, type DelayedFunc } from './delay.js'; -export { default as verifyLicense } from './verifylicense.js'; export { default as wait } from './wait.js'; +export { default as parseBase64EncodedObject } from './parsebase64encodedobject.js'; +export { default as crc32, type CRCData } from './crc32.js'; export * from './unicode.js'; diff --git a/packages/ckeditor5-utils/src/parsebase64encodedobject.ts b/packages/ckeditor5-utils/src/parsebase64encodedobject.ts new file mode 100644 index 00000000000..a3ed488462a --- /dev/null +++ b/packages/ckeditor5-utils/src/parsebase64encodedobject.ts @@ -0,0 +1,25 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module utils/parsebase64encodedobject + */ + +/** + * Parses a base64-encoded object and returns the decoded object, or null if the decoding was unsuccessful. + */ +export default function parseBase64EncodedObject( encoded: string ): Record | null { + try { + if ( !encoded.startsWith( 'ey' ) ) { + return null; + } + + const decoded = atob( encoded.replace( /-/g, '+' ).replace( /_/g, '/' ) ); + + return JSON.parse( decoded ); + } catch ( e ) { + return null; + } +} diff --git a/packages/ckeditor5-utils/src/verifylicense.ts b/packages/ckeditor5-utils/src/verifylicense.ts deleted file mode 100644 index 3b6a3dfd0fd..00000000000 --- a/packages/ckeditor5-utils/src/verifylicense.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -/** - * @module utils/verifylicense - */ - -import { releaseDate } from './version.js'; - -/** - * Possible states of the key after verification. - */ -export type VerifiedKeyStatus = 'VALID' | 'INVALID'; - -/** - * Checks whether the given string contains information that allows you to verify the license status. - * - * @param token The string to check. - * @returns String that represents the state of given `token` parameter. - */ -export default function verifyLicense( token: string | undefined ): VerifiedKeyStatus { - // This function implements naive and partial license key check mechanism, - // used only to decide whether to show or hide the "Powered by CKEditor" logo. - // - // You can read the reasoning behind showing the logo to unlicensed (GPL) users - // in this thread: https://github.com/ckeditor/ckeditor5/issues/14082. - // - // We firmly believe in the values behind creating open-source software, even when that - // means keeping the license verification logic open for everyone to see. - // - // Please keep this code intact. Thank you for your understanding. - - function oldTokenCheck( token: string ): VerifiedKeyStatus { - if ( token.length >= 40 && token.length <= 255 ) { - return 'VALID'; - } else { - return 'INVALID'; - } - } - - // TODO: issue ci#3175 - - if ( !token ) { - return 'INVALID'; - } - - let decryptedData = ''; - - try { - decryptedData = atob( token ); - } catch ( e ) { - return 'INVALID'; - } - - const splittedDecryptedData = decryptedData.split( '-' ); - - const firstElement = splittedDecryptedData[ 0 ]; - const secondElement = splittedDecryptedData[ 1 ]; - - if ( !secondElement ) { - return oldTokenCheck( token ); - } - - try { - atob( secondElement ); - } catch ( e ) { - try { - atob( firstElement ); - - if ( !atob( firstElement ).length ) { - return oldTokenCheck( token ); - } - } catch ( e ) { - return oldTokenCheck( token ); - } - } - - if ( firstElement.length < 40 || firstElement.length > 255 ) { - return 'INVALID'; - } - - let decryptedSecondElement = ''; - - try { - atob( firstElement ); - decryptedSecondElement = atob( secondElement ); - } catch ( e ) { - return 'INVALID'; - } - - if ( decryptedSecondElement.length !== 8 ) { - return 'INVALID'; - } - - const year = Number( decryptedSecondElement.substring( 0, 4 ) ); - const monthIndex = Number( decryptedSecondElement.substring( 4, 6 ) ) - 1; - const day = Number( decryptedSecondElement.substring( 6, 8 ) ); - - const date = new Date( year, monthIndex, day ); - - if ( date < releaseDate || isNaN( Number( date ) ) ) { - return 'INVALID'; - } - - return 'VALID'; -} diff --git a/packages/ckeditor5-utils/tests/crc32.js b/packages/ckeditor5-utils/tests/crc32.js new file mode 100644 index 00000000000..b263fa85cca --- /dev/null +++ b/packages/ckeditor5-utils/tests/crc32.js @@ -0,0 +1,99 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import crc32 from '../src/crc32.js'; + +describe( 'crc32', () => { + describe( 'input is a single value (not an array)', () => { + it( 'should correctly calculate the CRC32 checksum for a string', () => { + const input = 'foo'; + const expectedHex = '8c736521'; + expect( crc32( input ) ).to.equal( expectedHex ); + } ); + + it( 'should correctly calculate the CRC32 checksum for a number', () => { + const input = 123; + const expectedHex = '884863d2'; + expect( crc32( input ) ).to.equal( expectedHex ); + } ); + + it( 'should correctly calculate the CRC32 checksum for a boolean', () => { + const input = true; + const expectedHex = 'fdfc4c8d'; + expect( crc32( input ) ).to.equal( expectedHex ); + } ); + + it( 'should correctly calculate the CRC32 checksum for an empty string', () => { + const input = ''; + const expectedHex = '00000000'; + expect( crc32( input ) ).to.equal( expectedHex ); + } ); + } ); + + describe( 'input is an array', () => { + it( 'should correctly calculate the CRC32 checksum for a string', () => { + const input = [ 'foo' ]; + const expectedHex = '8c736521'; + expect( crc32( input ) ).to.equal( expectedHex ); + } ); + + it( 'should correctly calculate the CRC32 checksum for a number', () => { + const input = [ 123 ]; + const expectedHex = '884863d2'; + expect( crc32( input ) ).to.equal( expectedHex ); + } ); + + it( 'should correctly calculate the CRC32 checksum for a boolean', () => { + const input = [ true ]; + const expectedHex = 'fdfc4c8d'; + expect( crc32( input ) ).to.equal( expectedHex ); + } ); + + it( 'should correctly calculate the CRC32 checksum for a table of strings', () => { + const input = [ 'foo', 'bar', 'baz' ]; + const expectedHex = '1a7827aa'; + expect( crc32( input ) ).to.equal( expectedHex ); + } ); + + it( 'should handle mixed data types and compute a valid CRC32 checksum', () => { + const input = [ 'foo', 123, false, [ 'bar', 'baz' ] ]; + const expectedHex = 'ee1795af'; + expect( crc32( input ) ).to.equal( expectedHex ); + } ); + + it( 'should correctly handle an empty array', () => { + const input = []; + const expectedHex = '00000000'; + expect( crc32( input ) ).to.equal( expectedHex ); + } ); + + it( 'should correctly handle arrays containing empty strings', () => { + const input = [ '', '', '' ]; + const expectedHex = '00000000'; + expect( crc32( input ) ).to.equal( expectedHex ); + } ); + } ); + + describe( 'return values', () => { + it( 'should return a hexadecimal string when returnHex is true', () => { + const input = [ 'foo' ]; + const result = '8c736521'; + expect( crc32( input ) ).to.equal( result ); + } ); + + it( 'should return a hexadecimal string when returnHex is not set', () => { + const input = [ 'foo' ]; + const result = '8c736521'; + expect( crc32( input ) ).to.equal( result ); + } ); + + it( 'should return consistent results for the same input', () => { + const input = [ 'foo', 'bar' ]; + const firstRun = crc32( input ); + const secondRun = crc32( input ); + expect( firstRun ).to.equal( secondRun ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-utils/tests/parsebase64encodedobject.js b/packages/ckeditor5-utils/tests/parsebase64encodedobject.js new file mode 100644 index 00000000000..e673b4742b6 --- /dev/null +++ b/packages/ckeditor5-utils/tests/parsebase64encodedobject.js @@ -0,0 +1,36 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import parseBase64EncodedObject from '../src/parsebase64encodedobject.js'; + +/* globals btoa */ + +describe( 'parseBase64EncodedObject', () => { + it( 'should return a decoded object', () => { + const obj = { foo: 1 }; + const encoded = btoa( JSON.stringify( obj ) ); + + expect( parseBase64EncodedObject( encoded ) ).to.deep.equal( obj ); + } ); + + it( 'should return null if it is not an object', () => { + const str = 'foo'; + const encoded = btoa( JSON.stringify( str ) ); + + expect( parseBase64EncodedObject( encoded ) ).to.be.null; + } ); + + it( 'should return null of it is not parsable', () => { + const encoded = btoa( '{"foo":1' ); + + expect( parseBase64EncodedObject( encoded ) ).to.be.null; + } ); + + it( 'should use base64Safe variant of encoding', () => { + const encoded = 'eyJmb28iOiJhYmNkZW/n+Glqa2xtbm8ifQ=='; + + expect( parseBase64EncodedObject( encoded ) ).to.deep.equal( { foo: 'abcdeoçøijklmno' } ); + } ); +} ); diff --git a/packages/ckeditor5-utils/tests/verifylicense.js b/packages/ckeditor5-utils/tests/verifylicense.js deleted file mode 100644 index f59fa30e394..00000000000 --- a/packages/ckeditor5-utils/tests/verifylicense.js +++ /dev/null @@ -1,114 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -import verifyLicense from '../src/verifylicense.js'; - -describe( 'utils', () => { - describe( 'verify', () => { - describe( 'should return `VALID`', () => { - it( 'when date is later than the release date', () => { - // new, future - // eslint-disable-next-line max-len - const string = 'dG9vZWFzZXRtcHNsaXVyb3JsbWlkbXRvb2Vhc2V0bXBzbGl1cm9ybG1pZG10b29lYXNldG1wc2xpdXJvcmxtaWRtLU1qQTBOREEyTVRJPQ=='; - - expect( verifyLicense( string ) ).to.be.equal( 'VALID' ); - } ); - - it( 'when old token format is given', () => { - // old - // eslint-disable-next-line max-len - const string = 'YWZvb2JkcnphYXJhZWJvb290em9wbWJvbHJ1c21sZnJlYmFzdG1paXN1cm1tZmllenJhb2F0YmFvcmxvb3B6aWJvYWRiZWZzYXJ0bW9ibG8='; - - expect( verifyLicense( string ) ).to.be.equal( 'VALID' ); - } ); - - it( 'when old token format is given with a special sign', () => { - const string = 'LWRsZ2h2bWxvdWhnbXZsa3ZkaGdzZGhtdmxrc2htZ3Nma2xnaGxtcDk4N212Z3V3OTU4NHc5bWdtdw=='; - - expect( verifyLicense( string ) ).to.be.equal( 'VALID' ); - } ); - - it( 'when old token is splitted', () => { - // eslint-disable-next-line max-len - const string = 'ZXNybGl1aG1jbGlldWdtbHdpZWgvIUAjNW1nbGNlXVtcd2l1Z2NsZWpnbWNsc2lkZmdjbHNpZGZoZ2xjc2Rnc25jZGZnaGNubHMtd3A5bWN5dDlwaGdtcGM5d2g4dGc3Y3doODdvaGddW10hQCMhdG5jN293NTg0aGdjbzhud2U4Z2Nodw=='; - - expect( verifyLicense( string ) ).to.be.equal( 'VALID' ); - } ); - } ); - - describe( 'should return `INVALID`', () => { - it( 'when token is empty', () => { - expect( verifyLicense( '' ) ).to.be.equal( 'INVALID' ); - } ); - - it( 'when token is not passed', () => { - expect( verifyLicense( ) ).to.be.equal( 'INVALID' ); - } ); - - describe( 'new', () => { - it( 'first too short', () => { - expect( verifyLicense( 'Wm05dlltRnktTWpBeU5UQXhNREU9' ) ).to.be.equal( 'INVALID' ); - } ); - - it( 'first too long', () => { - // eslint-disable-next-line max-len - const string = 'YzNSbGJTQmxjbkp2Y2pvZ2JtVjBPanBGVWxKZlFreFBRMHRGUkY5Q1dWOURURWxGVGxSemRHVnRJR1Z5Y205eU9pQnVaWFE2T2tWU1VsOUNURTlEUzBWRVgwSlpYME5NU1VWT1ZITjBaVzBnWlhKeWIzSTZJRzVsZERvNlJWSlNYMEpNVDBOTFJVUmZRbGxmUTB4SlJVNVVjM1JsYlNCbGNuSnZjam9nYm1WME9qcEZVbEpmUWt4UFEwdEZSRjlDV1Y5RFRFbEZUbFJ6ZEdWdElHVnljbTl5T2lCdVpYUTZPa1ZTVWw5Q1RFOURTMFZFWDBKWlgwTk1TVVZPVkhOMFpXMGdaWEp5YjNJNklHNWxkRG82UlZKU1gwSk1UME5MUlVSZlFsbGZRMHhKUlU1VS1NakF5TlRBeE1ERT0='; - - expect( verifyLicense( string ) ).to.be.equal( 'INVALID' ); - } ); - - it( 'first wrong format', () => { - const string = 'ZGx1Z2hjbXNsaXVkZ2NobXN8IjolRVdFVnwifCJEVnxERyJXJSUkXkVSVHxWIll8UkRUIkJTfFIlQiItTWpBeU16RXlNekU9'; - - expect( verifyLicense( string ) ).to.be.equal( 'INVALID' ); - } ); - - it( 'when date is invalid', () => { - // invalid = shorten than expected - - const string = 'enN6YXJ0YWxhYWZsaWViYnRvcnVpb3Jvb3BzYmVkYW9tcm1iZm9vbS1NVGs1TnpFeA=='; - - expect( verifyLicense( string ) ).to.be.equal( 'INVALID' ); - } ); - - it( 'wrong second part format', () => { - const string = 'Ylc5MGIyeDBiMkZ5YzJsbGMybHNjR1Z0Y20xa2RXMXZkRzlzZEc5aGNuTnBaWE09LVptOXZZbUZ5WW1FPQ=='; - - expect( verifyLicense( string ) ).to.be.equal( 'INVALID' ); - } ); - - it( 'when wrong string passed', () => { - // # instead of second part - const string = 'Ylc5MGIyeDBiMkZ5YzJsbGMybHNjR1Z0Y20xa2RXMXZkRzlzZEc5aGNuTnBaWE09LSM='; - - expect( verifyLicense( string ) ).to.be.equal( 'INVALID' ); - } ); - - it( 'when date is earlier than the release date', () => { - // new, past - // eslint-disable-next-line max-len - const string = 'Y0dWc1lYVmxhWE4wYlc5a2MyMXBiRzEwY205eWIzQmxiR0YxWldsemRHMXZaSE50YVd4dGRISnZjbTlrYzJGa2MyRmtjMkU9LU1UazRNREF4TURFPQ=='; - - expect( verifyLicense( string ) ).to.be.equal( 'INVALID' ); - } ); - } ); - - describe( 'old', () => { - it( 'when date is missing', () => { - const string = 'dG1wb3Rhc2llc3VlbHJtZHJvbWlsbw=='; - - expect( verifyLicense( string ) ).to.be.equal( 'INVALID' ); - } ); - } ); - - it( 'when passed variable is invalid', () => { - // invalid - const string = 'foobarbaz'; - - expect( verifyLicense( string ) ).to.be.equal( 'INVALID' ); - } ); - } ); - } ); -} ); diff --git a/packages/ckeditor5-watchdog/docs/features/watchdog.md b/packages/ckeditor5-watchdog/docs/features/watchdog.md index 8b02bf5ca74..404b40376e3 100644 --- a/packages/ckeditor5-watchdog/docs/features/watchdog.md +++ b/packages/ckeditor5-watchdog/docs/features/watchdog.md @@ -31,7 +31,7 @@ There are two available types of watchdogs: ### Editor watchdog -After {@link getting-started/quick-start installing the editor}, change your `ClassicEditor.create()` call to `watchdog.create()` as follows: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, change your `ClassicEditor.create()` call to `watchdog.create()` as follows: ```js @@ -42,6 +42,7 @@ const watchdog = new EditorWatchdog( ClassicEditor ); // Create a new editor instance. watchdog.create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ Essentials, Paragraph, Bold, Italic ], toolbar: [ 'bold', 'italic', 'alignment' ] } ); @@ -139,7 +140,7 @@ watchdog.crashes.forEach( crashInfo => console.log( crashInfo ) ); ### Context watchdog -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, ContextWatchdog, Bold, Italic, Context, Essentials, Paragraph } from 'ckeditor5'; diff --git a/packages/ckeditor5-word-count/docs/_snippets/features/build-word-count-source.js b/packages/ckeditor5-word-count/docs/_snippets/features/build-word-count-source.js index bbfe163200c..a7ab54130e0 100644 --- a/packages/ckeditor5-word-count/docs/_snippets/features/build-word-count-source.js +++ b/packages/ckeditor5-word-count/docs/_snippets/features/build-word-count-source.js @@ -102,7 +102,8 @@ BalloonEditor.defaultConfig = { ] }, // This value must be kept in sync with the language defined in webpack.config.js. - language: 'en' + language: 'en', + licenseKey: 'GPL' }; class ClassicEditor extends ClassicEditorBase { @@ -150,7 +151,8 @@ ClassicEditor.defaultConfig = { ] }, // This value must be kept in sync with the language defined in webpack.config.js. - language: 'en' + language: 'en', + licenseKey: 'GPL' }; ClassicEditor.builtinPlugins.push( WordCount ); diff --git a/packages/ckeditor5-word-count/docs/_snippets/features/word-count-update.js b/packages/ckeditor5-word-count/docs/_snippets/features/word-count-update.js index 7a06b24aa59..3b99f011aa5 100644 --- a/packages/ckeditor5-word-count/docs/_snippets/features/word-count-update.js +++ b/packages/ckeditor5-word-count/docs/_snippets/features/word-count-update.js @@ -87,7 +87,8 @@ BalloonEditor // If the character limit is exceeded, disable the send button. sendButton.toggleAttribute( 'disabled', isLimitExceeded ); } - } + }, + licenseKey: 'GPL' } ) .catch( err => { console.error( err.stack ); diff --git a/packages/ckeditor5-word-count/docs/_snippets/features/word-count.js b/packages/ckeditor5-word-count/docs/_snippets/features/word-count.js index fb582285200..0dbb29e99da 100644 --- a/packages/ckeditor5-word-count/docs/_snippets/features/word-count.js +++ b/packages/ckeditor5-word-count/docs/_snippets/features/word-count.js @@ -44,7 +44,8 @@ ClassicEditor }, table: { contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ] - } + }, + licenseKey: 'GPL' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-word-count/docs/features/word-count.md b/packages/ckeditor5-word-count/docs/features/word-count.md index 9c7a874e772..16443e81957 100644 --- a/packages/ckeditor5-word-count/docs/features/word-count.md +++ b/packages/ckeditor5-word-count/docs/features/word-count.md @@ -52,14 +52,18 @@ ClassicEditor Starting with {@link updating/update-to-42 version 42.0.0}, we changed the format of import paths. This guide uses the new, shorter format. Refer to the {@link getting-started/legacy-getting-started/legacy-imports Packages in the legacy setup} guide if you use an older version of CKEditor 5. -After {@link getting-started/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: +After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the feature to your plugin list and toolbar configuration: ```js import { ClassicEditor, WordCount } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { + licenseKey: '', // Or 'GPL'. plugins: [ WordCount, /* ... */ ], + wordCount: { + // Configuration. + } } ) .then( /* ... */ ) .catch( /* ... */ ); @@ -101,7 +105,7 @@ You can execute your custom callback every time content statistics change by def ```js ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ WordCount, /* ... */ ], + // ... Other configuration options ... wordCount: { onUpdate: stats => { // Prints the current content statistics.