diff --git a/docs/_snippets/features/keyboard-support.html b/docs/_snippets/features/keyboard-support.html new file mode 100644 index 00000000000..6baf4d350a7 --- /dev/null +++ b/docs/_snippets/features/keyboard-support.html @@ -0,0 +1,6 @@ + + + +
+

Press Alt+0 (⌥0 on Mac) while editing to display the list of available keyboard shortcuts.

+
diff --git a/docs/_snippets/features/keyboard-support.js b/docs/_snippets/features/keyboard-support.js new file mode 100644 index 00000000000..4c6bd9145d6 --- /dev/null +++ b/docs/_snippets/features/keyboard-support.js @@ -0,0 +1,538 @@ +/** + * @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, document, open, console, LICENSE_KEY */ + +// Keep the guide listing updated with each change + +import { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic'; +import { Alignment } from '@ckeditor/ckeditor5-alignment'; +import { Autoformat } from '@ckeditor/ckeditor5-autoformat'; +import { Bold, Code, Italic, Strikethrough, Subscript, Superscript, Underline } from '@ckeditor/ckeditor5-basic-styles'; +import { BlockQuote } from '@ckeditor/ckeditor5-block-quote'; +import { CaseChange } from '@ckeditor/ckeditor5-case-change'; +import { CKBox, CKBoxImageEdit } from '@ckeditor/ckeditor5-ckbox'; +import { CloudServices } from '@ckeditor/ckeditor5-cloud-services'; +import { CodeBlock } from '@ckeditor/ckeditor5-code-block'; +import { TableOfContents } from '@ckeditor/ckeditor5-document-outline'; +import { Essentials } from '@ckeditor/ckeditor5-essentials'; +import { ExportPdf } from '@ckeditor/ckeditor5-export-pdf'; +import { ExportWord } from '@ckeditor/ckeditor5-export-word'; +import { FindAndReplace } from '@ckeditor/ckeditor5-find-and-replace'; +import { Font } from '@ckeditor/ckeditor5-font'; +import { FormatPainter } from '@ckeditor/ckeditor5-format-painter'; +import { GeneralHtmlSupport } from '@ckeditor/ckeditor5-html-support'; +import { Heading } from '@ckeditor/ckeditor5-heading'; +import { Highlight } from '@ckeditor/ckeditor5-highlight'; +import { HorizontalLine } from '@ckeditor/ckeditor5-horizontal-line'; +import { HtmlEmbed } from '@ckeditor/ckeditor5-html-embed'; +import { AutoImage, + Image, + ImageCaption, + ImageInsert, + ImageResize, + ImageStyle, + ImageToolbar, + ImageUpload, + PictureEditing +} from '@ckeditor/ckeditor5-image'; +import { ImportWord } from '@ckeditor/ckeditor5-import-word'; +import { Indent, IndentBlock } from '@ckeditor/ckeditor5-indent'; +import { AutoLink, Link, LinkImage } from '@ckeditor/ckeditor5-link'; +import { List, ListProperties, TodoList } from '@ckeditor/ckeditor5-list'; +import { MediaEmbed } from '@ckeditor/ckeditor5-media-embed'; +import { Mention } from '@ckeditor/ckeditor5-mention'; +import { PageBreak } from '@ckeditor/ckeditor5-page-break'; +import { Paragraph } from '@ckeditor/ckeditor5-paragraph'; +import { PasteFromOffice } from '@ckeditor/ckeditor5-paste-from-office'; +import { PasteFromOfficeEnhanced } from '@ckeditor/ckeditor5-paste-from-office-enhanced'; +import { RemoveFormat } from '@ckeditor/ckeditor5-remove-format'; +import { ShowBlocks } from '@ckeditor/ckeditor5-show-blocks'; +import { SlashCommand } from '@ckeditor/ckeditor5-slash-command'; +import { SourceEditing } from '@ckeditor/ckeditor5-source-editing'; +import { SpecialCharacters, SpecialCharactersEssentials } from '@ckeditor/ckeditor5-special-characters'; +import { Style } from '@ckeditor/ckeditor5-style'; +import { Table, TableCaption, TableCellProperties, TableColumnResize, TableProperties, TableToolbar } from '@ckeditor/ckeditor5-table'; +import { Template } from '@ckeditor/ckeditor5-template'; +import { TextTransformation } from '@ckeditor/ckeditor5-typing'; +import WProofreader from '@webspellchecker/wproofreader-ckeditor5/src/wproofreader.js'; + +// Additional protection for internal license keys CF#2555. +window.open.closed = 1; + +// import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; +import { TOKEN_URL } from '@ckeditor/ckeditor5-ckbox/tests/_utils/ckbox-config.js'; + +// Allow using internal license keys in this sample. See CF#2555. +open.closed = 1; + +// templates icons +import articleImageRightIcon from '../../assets/img/article-image-right.svg'; +import financialReportIcon from '../../assets/img/financial-report.svg'; +import formalLetterIcon from '../../assets/img/formal-letter.svg'; +import resumeIcon from '../../assets/img/resume.svg'; +import richTableIcon from '../../assets/img/rich-table.svg'; + +ClassicEditor + .create( document.querySelector( '#keyboard-support' ), { + // cloudServices: CS_CONFIG, + ui: { + viewportOffset: { + top: window.getViewportTopOffsetConfig() + } + }, + poweredBy: { + position: 'inside', + side: 'left', + label: 'This is' + }, + plugins: [ + Autoformat, BlockQuote, Bold, Heading, CaseChange, Image, ImageCaption, FormatPainter, + ImageStyle, ImageToolbar, Indent, Italic, Link, List, MediaEmbed, + Paragraph, Table, TableToolbar, Alignment, AutoImage, AutoLink, + CKBox, CKBoxImageEdit, CloudServices, Code, CodeBlock, Essentials, ExportPdf, + ExportWord, ImportWord, FindAndReplace, Font, Highlight, HorizontalLine, + HtmlEmbed, ImageInsert, ImageResize, ImageUpload, IndentBlock, GeneralHtmlSupport, + LinkImage, ListProperties, TodoList, Mention, PageBreak, PasteFromOffice, + PasteFromOfficeEnhanced, PictureEditing, RemoveFormat, ShowBlocks, SlashCommand, SourceEditing, + SpecialCharacters, SpecialCharactersEssentials, Style, Strikethrough, Subscript, Superscript, + TableCaption, TableCellProperties, TableColumnResize, + TableProperties, TableOfContents, Template, TextTransformation, + Underline, WProofreader + ], + toolbar: { + items: [ + 'accessibilityHelp', + '|', + 'undo', 'redo', + '|', + 'heading', + '|', + 'fontSize', 'fontFamily', + { + label: 'Font color', + icon: 'plus', + items: [ 'fontColor', 'fontBackgroundColor' ] + }, + '|', + 'bold', 'italic', 'underline', + { + label: 'Formatting', + icon: 'text', + items: [ 'strikethrough', 'subscript', 'superscript', 'code', 'horizontalLine', '|', 'removeFormat' ] + }, + 'specialCharacters', 'pageBreak', + '|', + 'link', 'insertImage', 'ckbox', 'insertTable', 'tableOfContents', 'insertTemplate', + { + label: 'Insert', + icon: 'plus', + items: [ 'highlight', 'blockQuote', 'mediaEmbed', 'codeBlock', 'htmlEmbed' ] + }, + '|', + 'alignment', + '|', + 'bulletedList', 'numberedList', 'todoList', 'outdent', 'indent' + ] + }, + htmlSupport: { + allow: [ + { + name: /^.*$/, + styles: true, + attributes: true, + classes: true + } + ] + }, + style: { + definitions: [ + { + name: 'Article category', + element: 'h3', + classes: [ 'category' ] + }, + { + name: 'Title', + element: 'h2', + classes: [ 'document-title' ] + }, + { + name: 'Subtitle', + element: 'h3', + classes: [ 'document-subtitle' ] + }, + { + name: 'Info box', + element: 'p', + classes: [ 'info-box' ] + }, + { + name: 'Side quote', + element: 'blockquote', + classes: [ 'side-quote' ] + }, + { + name: 'Marker', + element: 'span', + classes: [ 'marker' ] + }, + { + name: 'Spoiler', + element: 'span', + classes: [ 'spoiler' ] + }, + { + name: 'Code (dark)', + element: 'pre', + classes: [ 'fancy-code', 'fancy-code-dark' ] + }, + { + name: 'Code (bright)', + element: 'pre', + classes: [ 'fancy-code', 'fancy-code-bright' ] + } + ] + }, + exportPdf: { + stylesheets: [ + '../../assets/pagination-fonts.css', + 'EDITOR_STYLES', + '../../snippets/features/pagination/snippet.css', + '../../assets/pagination.css' + ], + fileName: 'export-pdf-demo.pdf', + appID: 'cke5-docs', + converterOptions: { + format: 'Tabloid', + margin_top: '20mm', + margin_bottom: '20mm', + margin_right: '24mm', + margin_left: '24mm', + page_orientation: 'portrait' + }, + tokenUrl: false + }, + exportWord: { + stylesheets: [ 'EDITOR_STYLES' ], + fileName: 'export-word-demo.docx', + appID: 'cke5-docs', + converterOptions: { + format: 'B4', + margin_top: '20mm', + margin_bottom: '20mm', + margin_right: '12mm', + margin_left: '12mm', + page_orientation: 'portrait' + }, + tokenUrl: false + }, + fontFamily: { + supportAllValues: true + }, + fontSize: { + options: [ 10, 12, 14, 'default', 18, 20, 22 ], + supportAllValues: true + }, + htmlEmbed: { + showPreviews: true + }, + image: { + styles: [ + 'alignCenter', + 'alignLeft', + 'alignRight' + ], + resizeOptions: [ + { + name: 'resizeImage:original', + label: 'Original', + value: null + }, + { + name: 'resizeImage:50', + label: '50%', + value: '50' + }, + { + name: 'resizeImage:75', + label: '75%', + value: '75' + } + ], + toolbar: [ + 'imageTextAlternative', 'toggleImageCaption', '|', + 'imageStyle:inline', 'imageStyle:wrapText', 'imageStyle:breakText', 'imageStyle:side', '|', + 'resizeImage', '|', 'ckboxImageEdit' + ] + }, + list: { + properties: { + styles: true, + startIndex: true, + reversed: true + } + }, + link: { + decorators: { + addTargetToExternalLinks: true, + defaultProtocol: 'https://', + toggleDownloadable: { + mode: 'manual', + label: 'Downloadable', + attributes: { + download: 'file' + } + } + } + }, + mention: { + feeds: [ + { + marker: '@', + feed: [ + '@apple', '@bears', '@brownie', '@cake', '@cake', '@candy', '@canes', '@chocolate', '@cookie', '@cotton', '@cream', + '@cupcake', '@danish', '@donut', '@dragée', '@fruitcake', '@gingerbread', '@gummi', '@ice', '@jelly-o', + '@liquorice', '@macaroon', '@marzipan', '@oat', '@pie', '@plum', '@pudding', '@sesame', '@snaps', '@soufflé', + '@sugar', '@sweet', '@topping', '@wafer' + ], + minimumCharacters: 0 + } + ] + }, + importWord: { + tokenUrl: false, + defaultStyles: true + }, + placeholder: 'Type or paste your content here!', + table: { + contentToolbar: [ + 'tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties', 'toggleTableCaption' + ] + }, + template: { + definitions: [ + { + title: 'Document with an image', + description: 'Simple heading with text and and and image.', + icon: articleImageRightIcon, + data: `

Title of the document

+
+ +
A caption of the image.
+
+

The content of the document. 

` + }, + { + title: 'Annual financial report', + description: 'A report that spells out the company\'s financial condition.', + icon: financialReportIcon, + data: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Metric nameYear
2019202020212022
Revenue$100,000.00$120,000.00$130,000.00$180,000.00
Operating expenses    
Interest    
Net profit    
+
` + }, + { + title: 'Resume', + description: 'A quick overview of candidate\'s professional qualifications.', + icon: resumeIcon, + data: `
+ + + +
+

John Doe

+

Address, Phone, e-mail, social media

+

Profile

+

A quick summary of who you are and what you specialize in.

+

Employment history

+ +

Skills

+ +

Education

+ ` + }, + { + title: 'Formal business letter', + description: 'A clear letter template for business communication.', + icon: formalLetterIcon, + data: () => `

${ new Date().toLocaleDateString() }

+

Company name,
Street Name, Number
Post code, City

+

 

+

Dear [First name],

+

Content of the letter. Content of the letter. Content of the letter. Content of the letter. + Content of the letter. + Content of the letter. Content of the letter. Content of the letter. Content of the letter. Content of the letter. + Content of the letter. Content of the letter. Content of the letter. Content of the letter. Content of the letter. + Content of the letter. Content of the letter. Content of the letter. Content of the letter. Content of the letter. + Content of the letter. Content of the letter. Content of the letter. Content of the letter. Content of the letter. + Content of the letter. Content of the letter. Content of the letter. Content of the letter. Content of the letter. + Content of the letter. Content of the letter. Content of the letter. Content of the letter. 

+

Kind regards,

+

Name Surname
Position, Company
Phone, E-mail

` + }, + { + title: 'Rich table', + description: 'A table with a colorful header.', + icon: richTableIcon, + data: `
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Column 1Column 2Column 3Column 4Column 5
     
     
     
     
     
+
Caption of the table
+
` + } + ] + }, + wproofreader: { + serviceId: '1:Eebp63-lWHbt2-ASpHy4-AYUpy2-fo3mk4-sKrza1-NsuXy4-I1XZC2-0u2F54-aqYWd1-l3Qf14-umd', + lang: 'auto', + srcUrl: 'https://svc.webspellchecker.net/spellcheck31/wscbundle/wscbundle.js' + }, + ckbox: { + tokenUrl: TOKEN_URL, + forceDemoLabel: true, + allowExternalImagesEditing: [ /^data:/, 'origin' ] + }, + licenseKey: LICENSE_KEY + } ) + .then( editor => { + window.editor = editor; + // Prevent showing a warning notification when user is pasting a content from MS Word or Google Docs. + window.preventPasteFromOfficeNotification = true; + + window.attachTourBalloon( { + target: window.findToolbarItem( editor.ui.view.toolbar, + item => item.label && item.label === 'Accessibility help' ), + text: 'Click to display keyboard shortcuts.', + editor, + tippyOptions: { + placement: 'top-start' + } + } ); + } ) + .catch( err => { + console.error( err ); + } ); diff --git a/docs/features/keyboard-support.md b/docs/features/keyboard-support.md index 4ca2014c735..f307b002720 100644 --- a/docs/features/keyboard-support.md +++ b/docs/features/keyboard-support.md @@ -221,6 +221,16 @@ Use the following keystrokes for more efficient navigation in the CKEditor  } +## Displaying keyboard shortcuts in the editor + +CKEditor 5 offers a dedicated {@link module:ui/editorui/accessibilityhelp/accessibilityhelp~AccessibilityHelp Accessibility help} plugin that displays a list of all available keyboard shortcuts in a dialog. It can be opened by pressing Alt + 0 (on Windows) or ⌥0 (on macOS). Alternatively, you can use the toolbar button to open the dialog. + +{@snippet features/keyboard-support} + +The Accessibility help plugin is enabled by the {@link module:essentials/essentials~Essentials} plugin from the {@link api/essentials `@ckeditor/ckeditor5-essentials`} package (which also enables other fundamental editing features). + +Learn how to {@link tutorials/crash-course/keystrokes#adding-shortcut-information-to-the-accessibility-help-dialog add your own keyboard shortcuts} to the Accessibility help dialog. + ## Related productivity features Besides using keyboard shortcuts, you may want to check the following productivity features: diff --git a/docs/installation/advanced/using-two-editors.md b/docs/installation/advanced/using-two-editors.md index d7e2e5bf850..b9028af72ac 100644 --- a/docs/installation/advanced/using-two-editors.md +++ b/docs/installation/advanced/using-two-editors.md @@ -29,91 +29,88 @@ You can start by forking (or copying) an existing build like in the {@link insta ```bash git clone -b stable git@github.com:/ckeditor5.git cd ckeditor5/packages/ckeditor5-build-classic -npm install +yarn install ``` Now it is time to add the missing editor package and install it: -``` -npm install --save-dev @ckeditor/ckeditor5-editor-inline +```bash +yarn add -D @ckeditor/ckeditor5-editor-inline ``` -Once all the dependencies are installed, change the webpack's entry point which is the `src/ckeditor.js` file. For now, it was exporting just a single class: +Once all the dependencies are installed, you will need to modify the `src/ckeditor.ts` file, which currently only exports a single class. The first step is to move all plugins and configuration to variables so they can be reused by both editors: ```js -// The editor creator to use. import { ClassicEditor as ClassicEditorBase } from '@ckeditor/ckeditor5-editor-classic'; -// ... +// Other imports -export default class ClassicEditor extends ClassicEditorBase {} +class ClassicEditor extends ClassicEditorBase {} -// Plugins to include in the build. -ClassicEditor.builtinPlugins = [ - // ... -]; +const plugins = [ + // ... +] -// Editor configuration. -ClassicEditor.defaultConfig = { - // ... +const config = { + // ... +} + +ClassicEditor.builtinPlugins = plugins; +ClassicEditor.defaultConfig = config; + +export { + ClassicEditor }; ``` -Let's make it export an object with two classes: `ClassicEditor` and `InlineEditor`. To make both constructors work in the same way (load the same plugins and default configuration) you also need to assign `builtinPlugins` and `defaultConfig` static properties to both of them: +Now you can add the `InlineEditor` class to the file, add the same plugins and configuration to it and export it: -```js -// The editor creators to use. +```diff import { ClassicEditor as ClassicEditorBase } from '@ckeditor/ckeditor5-editor-classic'; -import { InlineEditor as InlineEditorBase } from '@ckeditor/ckeditor5-editor-inline'; ++ import { InlineEditor as InlineEditorBase } from '@ckeditor/ckeditor5-editor-inline'; -// ... +// Other imports class ClassicEditor extends ClassicEditorBase {} -class InlineEditor extends InlineEditorBase {} ++ class InlineEditor extends InlineEditorBase {} -// Plugins to include in the build. const plugins = [ - // ... + // ... ]; -ClassicEditor.builtinPlugins = plugins; -InlineEditor.builtinPlugins = plugins; - // Editor configuration. const config = { - // ... + // ... }; +ClassicEditor.builtinPlugins = plugins; ClassicEditor.defaultConfig = config; -InlineEditor.defaultConfig = config; + ++ InlineEditor.builtinPlugins = plugins; ++ InlineEditor.defaultConfig = config; export default { - ClassicEditor, InlineEditor + ClassicEditor, ++ InlineEditor }; ``` -Since you now export an object with two properties (`ClassicEditor` and `InlineEditor`), it is also reasonable to rename the global variable to which webpack will assign this object. It was called `ClassicEditor` before. An adequate name now would be, for example, `CKEDITOR`. This variable is defined in `webpack.config.js` in the `output.library` setting: +Since you now export an object with two editor types (`ClassicEditor` and `InlineEditor`), it is also reasonable to rename the global variable `ClassicEditor`. An appropriate name now might be `CKEDITOR`. This variable is defined in `webpack.config.js` in the `output.library` setting: ```diff -diff --git a/webpack.config.js b/webpack.config.js -index c57e371..04fc9fe 100644 ---- a/webpack.config.js -+++ b/webpack.config.js -@@ -21,7 +21,7 @@ module.exports = { - - output: { - // The name under which the editor will be exported. -- library: 'ClassicEditor', -+ library: 'CKEDITOR', - - path: path.resolve( __dirname, 'build' ), - filename: 'ckeditor.js', +// webpack.config.js + +module.exports = { + output: { +- library: 'ClassicEditor', ++ library: 'CKEDITOR', + // ... ``` -Once you changed the `src/ckeditor.js` and `webpack.config.js` files, it is time to rebuild the build: +Once you changed the `src/ckeditor.ts` and `webpack.config.js` files, it is time to rebuild the build: ```bash -npm run build +yarn build ``` Finally, when webpack finishes compiling your super build, you can change the `samples/index.html` file to test both editors: diff --git a/docs/installation/getting-started/getting-and-setting-data.md b/docs/installation/getting-started/getting-and-setting-data.md index e27909bbc90..f156aac075f 100644 --- a/docs/installation/getting-started/getting-and-setting-data.md +++ b/docs/installation/getting-started/getting-and-setting-data.md @@ -112,31 +112,29 @@ Please note that the replaced ` ``` Thanks to that, the ` ``` While simple content like that mentioned above does not itself require to be encoded, encoding the data will prevent losing text like "<" or "<img>". - ## Updating the source element diff --git a/docs/tutorials/crash-course/keystrokes.md b/docs/tutorials/crash-course/keystrokes.md index 1719b530d25..f4f0178a60e 100644 --- a/docs/tutorials/crash-course/keystrokes.md +++ b/docs/tutorials/crash-course/keystrokes.md @@ -30,6 +30,29 @@ editor.keystrokes.set( 'Ctrl+Alt+H', ( event, cancel ) => { } ); ``` +## Adding shortcut information to the Accessibility help dialog + +The {@link features/keyboard-support#displaying-keyboard-shortcuts-in-the-editor Accessibility help} dialog displays a complete list of available keyboard shortcuts with their descriptions. It does not know about the [shortcut we just added](#adding-keyboard-shortcuts), though. + +The dialog reads from the {@link module:core/accessibility~Accessibility `editor.accessibility`} namespace where all the information about keystrokes and their accessible labels is stored. There is an API to add new entries ({@link module:core/accessibility~Accessibility#addKeystrokeInfos}, {@link module:core/accessibility~Accessibility#addKeystrokeInfoGroup}, and {@link module:core/accessibility~Accessibility#addKeystrokeInfoCategory} methods). + +In this case, a simple `editor.accessibility.addKeystrokeInfos( ... )` is all you need for the Accessibility help dialog to learn about the new shortcut: + +```js +const t = editor.t; + +editor.accessibility.addKeystrokeInfos( { + keystrokes: [ + { + label: t( 'Highlight text' ), + keystroke: 'Ctrl+Alt+H' + } + ] +} ); +``` + +You can learn more about the {@link module:ui/editorui/accessibilityhelp/accessibilityhelp~AccessibilityHelp} plugin and the {@link module:core/accessibility~Accessibility `editor.accessibility`} namespace in the API reference. + ## Updating button tooltip When you hover over the "Undo" and "Redo" buttons, you will see a tooltip containing the name of the operation and their respective keyboard shortcuts. However, when hovering over the "Highlight" button, the keyboard shortcut is missing. diff --git a/docs/tutorials/crash-course/model-and-schema.md b/docs/tutorials/crash-course/model-and-schema.md index 4e33c8894af..d5378ad4915 100644 --- a/docs/tutorials/crash-course/model-and-schema.md +++ b/docs/tutorials/crash-course/model-and-schema.md @@ -21,7 +21,7 @@ Later you will also learn about the **editing UI**, but for now, all you need to ### Model -The first and most important part of the editing engine is the model. The model is an HTML-like structure that represents the content of the editor. When the {@link module:core/editor/editor~Editor#setData `editor.setData()`} method is called, HTML passed as the argument is converted into the model. Then, when the {@link module:core/editor/editor~Editor#getData `model.getData()`} method is called, the model is converted back to HTML. +The first and most important part of the editing engine is the model. The model is an HTML-like structure that represents the content of the editor. When the {@link module:core/editor/editor~Editor#setData `editor.setData()`} method is called, HTML passed as the argument is converted into the model. Then, when the {@link module:core/editor/editor~Editor#getData `editor.getData()`} method is called, the model is converted back to HTML. One major difference between the model and HTML is that in the model, both text and elements can have attributes. diff --git a/packages/ckeditor5-autoformat/lang/contexts.json b/packages/ckeditor5-autoformat/lang/contexts.json new file mode 100644 index 00000000000..011e96880d3 --- /dev/null +++ b/packages/ckeditor5-autoformat/lang/contexts.json @@ -0,0 +1,3 @@ +{ + "Revert autoformatting action": "Keystroke description for assistive technologies: keystroke for reverting autoformatting action." +} diff --git a/packages/ckeditor5-autoformat/src/autoformat.ts b/packages/ckeditor5-autoformat/src/autoformat.ts index d24b056f890..f3afeb42b7d 100644 --- a/packages/ckeditor5-autoformat/src/autoformat.ts +++ b/packages/ckeditor5-autoformat/src/autoformat.ts @@ -40,12 +40,25 @@ export default class Autoformat extends Plugin { * @inheritDoc */ public afterInit(): void { + const editor = this.editor; + const t = this.editor.t; + this._addListAutoformats(); this._addBasicStylesAutoformats(); this._addHeadingAutoformats(); this._addBlockQuoteAutoformats(); this._addCodeBlockAutoformats(); this._addHorizontalLineAutoformats(); + + // Add the information about the keystroke to the accessibility database. + editor.accessibility.addKeystrokeInfos( { + keystrokes: [ + { + label: t( 'Revert autoformatting action' ), + keystroke: 'Backspace' + } + ] + } ); } /** diff --git a/packages/ckeditor5-autoformat/tests/autoformat.js b/packages/ckeditor5-autoformat/tests/autoformat.js index f5749ee10f0..2ff76503aab 100644 --- a/packages/ckeditor5-autoformat/tests/autoformat.js +++ b/packages/ckeditor5-autoformat/tests/autoformat.js @@ -62,6 +62,17 @@ describe( 'Autoformat', () => { return editor.destroy(); } ); + it( 'should have pluginName', () => { + expect( Autoformat.pluginName ).to.equal( 'Autoformat' ); + } ); + + it( 'should add keystroke accessibility info', () => { + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'common' ).keystrokes ).to.deep.include( { + label: 'Revert autoformatting action', + keystroke: 'Backspace' + } ); + } ); + describe( 'Bulleted list', () => { it( 'should replace asterisk with bulleted list item', () => { setData( model, '*[]' ); diff --git a/packages/ckeditor5-basic-styles/lang/contexts.json b/packages/ckeditor5-basic-styles/lang/contexts.json index 6f0d9a8e64f..581582cc737 100644 --- a/packages/ckeditor5-basic-styles/lang/contexts.json +++ b/packages/ckeditor5-basic-styles/lang/contexts.json @@ -5,5 +5,10 @@ "Code": "Toolbar button tooltip for the Code feature.", "Strikethrough": "Toolbar button tooltip for the Strikethrough feature.", "Subscript": "Toolbar button tooltip for the Subscript feature.", - "Superscript": "Toolbar button tooltip for the Superscript feature." + "Superscript": "Toolbar button tooltip for the Superscript feature.", + "Italic text": "Keystroke description for assistive technologies: keystroke for making text italic.", + "Move out of an inline code style": "Keystroke description for assistive technologies: keystroke for moving selection out of an inline code style.", + "Bold text": "Keystroke description for assistive technologies: keystroke for making text bold.", + "Underline text": "Keystroke description for assistive technologies: keystroke for making text underlined.", + "Strikethrough text": "Keystroke description for assistive technologies: keystroke for making text strikethrough." } diff --git a/packages/ckeditor5-basic-styles/src/bold/boldediting.ts b/packages/ckeditor5-basic-styles/src/bold/boldediting.ts index f330eb539f4..1727a4d3396 100644 --- a/packages/ckeditor5-basic-styles/src/bold/boldediting.ts +++ b/packages/ckeditor5-basic-styles/src/bold/boldediting.ts @@ -31,6 +31,8 @@ export default class BoldEditing extends Plugin { */ public init(): void { const editor = this.editor; + const t = this.editor.t; + // Allow bold attribute on text nodes. editor.model.schema.extend( '$text', { allowAttributes: BOLD } ); editor.model.schema.setAttributeProperties( BOLD, { @@ -69,5 +71,15 @@ export default class BoldEditing extends Plugin { // Set the Ctrl+B keystroke. editor.keystrokes.set( 'CTRL+B', BOLD ); + + // Add the information about the keystroke to the accessibility database. + editor.accessibility.addKeystrokeInfos( { + keystrokes: [ + { + label: t( 'Bold text' ), + keystroke: 'CTRL+B' + } + ] + } ); } } diff --git a/packages/ckeditor5-basic-styles/src/code/codeediting.ts b/packages/ckeditor5-basic-styles/src/code/codeediting.ts index bf113d7c2aa..0e84a7f65fa 100644 --- a/packages/ckeditor5-basic-styles/src/code/codeediting.ts +++ b/packages/ckeditor5-basic-styles/src/code/codeediting.ts @@ -41,6 +41,7 @@ export default class CodeEditing extends Plugin { */ public init(): void { const editor = this.editor; + const t = this.editor.t; // Allow code attribute on text nodes. editor.model.schema.extend( '$text', { allowAttributes: CODE } ); @@ -67,5 +68,18 @@ export default class CodeEditing extends Plugin { // Setup highlight over selected element. inlineHighlight( editor, CODE, 'code', HIGHLIGHT_CLASS ); + + // Add the information about the keystroke to the accessibility database. + editor.accessibility.addKeystrokeInfos( { + keystrokes: [ + { + label: t( 'Move out of an inline code style' ), + keystroke: [ + [ 'arrowleft', 'arrowleft' ], + [ 'arrowright', 'arrowright' ] + ] + } + ] + } ); } } diff --git a/packages/ckeditor5-basic-styles/src/italic/italicediting.ts b/packages/ckeditor5-basic-styles/src/italic/italicediting.ts index a3a6cdb2848..d23efc6d4a8 100644 --- a/packages/ckeditor5-basic-styles/src/italic/italicediting.ts +++ b/packages/ckeditor5-basic-styles/src/italic/italicediting.ts @@ -31,6 +31,7 @@ export default class ItalicEditing extends Plugin { */ public init(): void { const editor = this.editor; + const t = this.editor.t; // Allow italic attribute on text nodes. editor.model.schema.extend( '$text', { allowAttributes: ITALIC } ); @@ -57,5 +58,15 @@ export default class ItalicEditing extends Plugin { // Set the Ctrl+I keystroke. editor.keystrokes.set( 'CTRL+I', ITALIC ); + + // Add the information about the keystroke to the accessibility database. + editor.accessibility.addKeystrokeInfos( { + keystrokes: [ + { + label: t( 'Italic text' ), + keystroke: 'CTRL+I' + } + ] + } ); } } diff --git a/packages/ckeditor5-basic-styles/src/strikethrough/strikethroughediting.ts b/packages/ckeditor5-basic-styles/src/strikethrough/strikethroughediting.ts index 21425c4c344..557b712301e 100644 --- a/packages/ckeditor5-basic-styles/src/strikethrough/strikethroughediting.ts +++ b/packages/ckeditor5-basic-styles/src/strikethrough/strikethroughediting.ts @@ -32,6 +32,7 @@ export default class StrikethroughEditing extends Plugin { */ public init(): void { const editor = this.editor; + const t = this.editor.t; // Allow strikethrough attribute on text nodes. editor.model.schema.extend( '$text', { allowAttributes: STRIKETHROUGH } ); @@ -59,5 +60,15 @@ export default class StrikethroughEditing extends Plugin { // Set the Ctrl+Shift+X keystroke. editor.keystrokes.set( 'CTRL+SHIFT+X', 'strikethrough' ); + + // Add the information about the keystroke to the accessibility database. + editor.accessibility.addKeystrokeInfos( { + keystrokes: [ + { + label: t( 'Strikethrough text' ), + keystroke: 'CTRL+SHIFT+X' + } + ] + } ); } } diff --git a/packages/ckeditor5-basic-styles/src/underline/underlineediting.ts b/packages/ckeditor5-basic-styles/src/underline/underlineediting.ts index f4d649c766e..4a992a8ea49 100644 --- a/packages/ckeditor5-basic-styles/src/underline/underlineediting.ts +++ b/packages/ckeditor5-basic-styles/src/underline/underlineediting.ts @@ -31,6 +31,7 @@ export default class UnderlineEditing extends Plugin { */ public init(): void { const editor = this.editor; + const t = this.editor.t; // Allow strikethrough attribute on text nodes. editor.model.schema.extend( '$text', { allowAttributes: UNDERLINE } ); @@ -54,5 +55,15 @@ export default class UnderlineEditing extends Plugin { // Set the Ctrl+U keystroke. editor.keystrokes.set( 'CTRL+U', 'underline' ); + + // Add the information about the keystroke to the accessibility database. + editor.accessibility.addKeystrokeInfos( { + keystrokes: [ + { + label: t( 'Underline text' ), + keystroke: 'CTRL+U' + } + ] + } ); } } diff --git a/packages/ckeditor5-basic-styles/tests/bold/boldediting.js b/packages/ckeditor5-basic-styles/tests/bold/boldediting.js index 5fad45b4bb3..44a91de313c 100644 --- a/packages/ckeditor5-basic-styles/tests/bold/boldediting.js +++ b/packages/ckeditor5-basic-styles/tests/bold/boldediting.js @@ -40,6 +40,13 @@ describe( 'BoldEditing', () => { expect( editor.plugins.get( BoldEditing ) ).to.be.instanceOf( BoldEditing ); } ); + it( 'should add keystroke accessibility info', () => { + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'common' ).keystrokes ).to.deep.include( { + label: 'Bold text', + keystroke: 'CTRL+B' + } ); + } ); + it( 'should set proper schema rules', () => { expect( model.schema.checkAttribute( [ '$root', '$block', '$text' ], 'bold' ) ).to.be.true; expect( model.schema.checkAttribute( [ '$clipboardHolder', '$text' ], 'bold' ) ).to.be.true; diff --git a/packages/ckeditor5-basic-styles/tests/code/codeediting.js b/packages/ckeditor5-basic-styles/tests/code/codeediting.js index 33ff0c9aa38..1c6c1d4666f 100644 --- a/packages/ckeditor5-basic-styles/tests/code/codeediting.js +++ b/packages/ckeditor5-basic-styles/tests/code/codeediting.js @@ -41,6 +41,16 @@ describe( 'CodeEditing', () => { expect( editor.plugins.get( CodeEditing ) ).to.be.instanceOf( CodeEditing ); } ); + it( 'should add keystroke accessibility info', () => { + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'common' ).keystrokes ).to.deep.include( { + label: 'Move out of an inline code style', + keystroke: [ + [ 'arrowleft', 'arrowleft' ], + [ 'arrowright', 'arrowright' ] + ] + } ); + } ); + it( 'should set proper schema rules', () => { expect( model.schema.checkAttribute( [ '$root', '$block', '$text' ], 'code' ) ).to.be.true; expect( model.schema.checkAttribute( [ '$clipboardHolder', '$text' ], 'code' ) ).to.be.true; diff --git a/packages/ckeditor5-basic-styles/tests/italic/italicediting.js b/packages/ckeditor5-basic-styles/tests/italic/italicediting.js index 2900bc67eec..cbeeb7d164f 100644 --- a/packages/ckeditor5-basic-styles/tests/italic/italicediting.js +++ b/packages/ckeditor5-basic-styles/tests/italic/italicediting.js @@ -38,6 +38,13 @@ describe( 'ItalicEditing', () => { expect( editor.plugins.get( ItalicEditing ) ).to.be.instanceOf( ItalicEditing ); } ); + it( 'should add keystroke accessibility info', () => { + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'common' ).keystrokes ).to.deep.include( { + label: 'Italic text', + keystroke: 'CTRL+I' + } ); + } ); + it( 'should set proper schema rules', () => { expect( model.schema.checkAttribute( [ '$root', '$block', '$text' ], 'italic' ) ).to.be.true; expect( model.schema.checkAttribute( [ '$clipboardHolder', '$text' ], 'italic' ) ).to.be.true; diff --git a/packages/ckeditor5-basic-styles/tests/strikethrough/strikethroughediting.js b/packages/ckeditor5-basic-styles/tests/strikethrough/strikethroughediting.js index df6ed0dca94..a9673b978c2 100644 --- a/packages/ckeditor5-basic-styles/tests/strikethrough/strikethroughediting.js +++ b/packages/ckeditor5-basic-styles/tests/strikethrough/strikethroughediting.js @@ -38,6 +38,13 @@ describe( 'StrikethroughEditing', () => { expect( editor.plugins.get( StrikethroughEditing ) ).to.be.instanceOf( StrikethroughEditing ); } ); + it( 'should add keystroke accessibility info', () => { + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'common' ).keystrokes ).to.deep.include( { + label: 'Strikethrough text', + keystroke: 'CTRL+SHIFT+X' + } ); + } ); + it( 'should set proper schema rules', () => { expect( model.schema.checkAttribute( [ '$root', '$block', '$text' ], 'strikethrough' ) ).to.be.true; expect( model.schema.checkAttribute( [ '$clipboardHolder', '$text' ], 'strikethrough' ) ).to.be.true; diff --git a/packages/ckeditor5-basic-styles/tests/underline/underlineediting.js b/packages/ckeditor5-basic-styles/tests/underline/underlineediting.js index c6950a769e6..af92f65398f 100644 --- a/packages/ckeditor5-basic-styles/tests/underline/underlineediting.js +++ b/packages/ckeditor5-basic-styles/tests/underline/underlineediting.js @@ -38,6 +38,13 @@ describe( 'UnderlineEditing', () => { expect( editor.plugins.get( UnderlineEditing ) ).to.be.instanceOf( UnderlineEditing ); } ); + it( 'should add keystroke accessibility info', () => { + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'common' ).keystrokes ).to.deep.include( { + label: 'Underline text', + keystroke: 'CTRL+U' + } ); + } ); + it( 'should set proper schema rules', () => { expect( model.schema.checkAttribute( [ '$root', '$block', '$text' ], 'underline' ) ).to.be.true; expect( model.schema.checkAttribute( [ '$clipboardHolder', '$text' ], 'underline' ) ).to.be.true; diff --git a/packages/ckeditor5-ckbox/src/ckboxediting.ts b/packages/ckeditor5-ckbox/src/ckboxediting.ts index 0abe116227c..7efd7e7b23e 100644 --- a/packages/ckeditor5-ckbox/src/ckboxediting.ts +++ b/packages/ckeditor5-ckbox/src/ckboxediting.ts @@ -62,17 +62,29 @@ export default class CKBoxEditing extends Plugin { */ public init(): void { const editor = this.editor; - const hasConfiguration = !!editor.config.get( 'ckbox' ); - const isLibraryLoaded = !!window.CKBox; - // Proceed with plugin initialization only when the integrator intentionally wants to use it, i.e. when the `config.ckbox` exists or - // the CKBox JavaScript library is loaded. - if ( !hasConfiguration && !isLibraryLoaded ) { + if ( !this._shouldBeInitialised() ) { return; } this._checkImagePlugins(); + // Registering the `ckbox` command makes sense only if the CKBox library is loaded, as the `ckbox` command opens the CKBox dialog. + if ( isLibraryLoaded() ) { + editor.commands.add( 'ckbox', new CKBoxCommand( editor ) ); + } + } + + /** + * @inheritdoc + */ + public afterInit(): void { + const editor = this.editor; + + if ( !this._shouldBeInitialised() ) { + return; + } + // Extending the schema, registering converters and applying fixers only make sense if the configuration option to assign // the assets ID with the model elements is enabled. if ( !editor.config.get( 'ckbox.ignoreDataId' ) ) { @@ -80,11 +92,17 @@ export default class CKBoxEditing extends Plugin { this._initConversion(); this._initFixers(); } + } - // Registering the `ckbox` command makes sense only if the CKBox library is loaded, as the `ckbox` command opens the CKBox dialog. - if ( isLibraryLoaded ) { - editor.commands.add( 'ckbox', new CKBoxCommand( editor ) ); - } + /** + * Returns true only when the integrator intentionally wants to use the plugin, i.e. when the `config.ckbox` exists or + * the CKBox JavaScript library is loaded. + */ + private _shouldBeInitialised(): boolean { + const editor = this.editor; + const hasConfiguration = !!editor.config.get( 'ckbox' ); + + return hasConfiguration || isLibraryLoaded(); } /** @@ -421,3 +439,10 @@ function shouldUpcastAttributeForNode( node: Node ) { return false; } + +/** + * Returns true if the CKBox library is loaded, false otherwise. + */ +function isLibraryLoaded(): boolean { + return !!window.CKBox; +} diff --git a/packages/ckeditor5-ckbox/src/ckboxui.ts b/packages/ckeditor5-ckbox/src/ckboxui.ts index aa5ac222a20..7f52b340d43 100644 --- a/packages/ckeditor5-ckbox/src/ckboxui.ts +++ b/packages/ckeditor5-ckbox/src/ckboxui.ts @@ -31,10 +31,9 @@ export default class CKBoxUI extends Plugin { public afterInit(): void { const editor = this.editor; - const command: CKBoxCommand | undefined = editor.commands.get( 'ckbox' ); - // Do not register the `ckbox` button if the command does not exist. - if ( !command ) { + // This might happen when CKBox library is not loaded on the page. + if ( !editor.commands.get( 'ckbox' ) ) { return; } @@ -42,6 +41,7 @@ export default class CKBoxUI extends Plugin { const componentFactory = editor.ui.componentFactory; componentFactory.add( 'ckbox', locale => { + const command: CKBoxCommand = editor.commands.get( 'ckbox' )!; const button = new ButtonView( locale ); button.set( { @@ -64,7 +64,7 @@ export default class CKBoxUI extends Plugin { imageInsertUI.registerIntegration( { name: 'assetManager', - observable: command, + observable: () => editor.commands.get( 'ckbox' )!, buttonViewCreator: () => { const button = this.editor.ui.componentFactory.create( 'ckbox' ) as ButtonView; diff --git a/packages/ckeditor5-ckbox/tests/ckboxediting.js b/packages/ckeditor5-ckbox/tests/ckboxediting.js index f494468b9c3..f43349dcad8 100644 --- a/packages/ckeditor5-ckbox/tests/ckboxediting.js +++ b/packages/ckeditor5-ckbox/tests/ckboxediting.js @@ -187,6 +187,36 @@ describe( 'CKBoxEditing', () => { await editor.destroy(); } ); + + describe( 'CKBox loaded before the ImageBlock and ImageInline plugins', () => { + let editor, model, originalCKBox; + + beforeEach( async () => { + TokenMock.initialToken = 'ckbox-token'; + + originalCKBox = window.CKBox; + window.CKBox = {}; + + editor = await createTestEditor( { + ckbox: { + tokenUrl: 'http://cs.example.com' + } + }, true ); + + model = editor.model; + } ); + + afterEach( async () => { + window.CKBox = originalCKBox; + await editor.destroy(); + } ); + + // https://github.com/ckeditor/ckeditor5/issues/15581 + it( 'should extend the schema rules for imageBlock and imageInline', () => { + expect( model.schema.checkAttribute( [ '$root', 'imageBlock' ], 'ckboxImageId' ) ).to.be.true; + expect( model.schema.checkAttribute( [ '$root', '$block', 'imageInline' ], 'ckboxImageId' ) ).to.be.true; + } ); + } ); } ); describe( 'conversion', () => { @@ -1802,22 +1832,29 @@ describe( 'CKBoxEditing', () => { } ); } ); -function createTestEditor( config = {} ) { +function createTestEditor( config = {}, loadCKBoxFirst = false ) { + const plugins = [ + Paragraph, + ImageBlockEditing, + ImageInlineEditing, + ImageCaptionEditing, + LinkEditing, + LinkImageEditing, + PictureEditing, + ImageUploadEditing, + ImageUploadProgress, + CloudServices, + CKBoxUploadAdapter + ]; + + if ( loadCKBoxFirst ) { + plugins.unshift( CKBoxEditing ); + } else { + plugins.push( CKBoxEditing ); + } + return VirtualTestEditor.create( { - plugins: [ - Paragraph, - ImageBlockEditing, - ImageInlineEditing, - ImageCaptionEditing, - LinkEditing, - LinkImageEditing, - PictureEditing, - ImageUploadEditing, - ImageUploadProgress, - CloudServices, - CKBoxUploadAdapter, - CKBoxEditing - ], + plugins, substitutePlugins: [ CloudServicesCoreMock ], diff --git a/packages/ckeditor5-ckfinder/src/ckfinderui.ts b/packages/ckeditor5-ckfinder/src/ckfinderui.ts index 07f6a924c3d..c80a68c44be 100644 --- a/packages/ckeditor5-ckfinder/src/ckfinderui.ts +++ b/packages/ckeditor5-ckfinder/src/ckfinderui.ts @@ -55,11 +55,10 @@ export default class CKFinderUI extends Plugin { if ( editor.plugins.has( 'ImageInsertUI' ) ) { const imageInsertUI: ImageInsertUI = editor.plugins.get( 'ImageInsertUI' ); - const command: CKFinderCommand = editor.commands.get( 'ckfinder' )!; imageInsertUI.registerIntegration( { name: 'assetManager', - observable: command, + observable: () => editor.commands.get( 'ckfinder' )!, buttonViewCreator: () => { const button = this.editor.ui.componentFactory.create( 'ckfinder' ) as ButtonView; diff --git a/packages/ckeditor5-clipboard/lang/contexts.json b/packages/ckeditor5-clipboard/lang/contexts.json new file mode 100644 index 00000000000..b216dee3e56 --- /dev/null +++ b/packages/ckeditor5-clipboard/lang/contexts.json @@ -0,0 +1,5 @@ +{ + "Copy selected content": "Keystroke description for assistive technologies: keystroke for copying selected content.", + "Paste content": "Keystroke description for assistive technologies: keystroke for pasting content.", + "Paste content as plain text": "Keystroke description for assistive technologies: keystroke for pasting content as plain text." +} diff --git a/packages/ckeditor5-clipboard/src/clipboard.ts b/packages/ckeditor5-clipboard/src/clipboard.ts index cccf61abf60..486c848ae2d 100644 --- a/packages/ckeditor5-clipboard/src/clipboard.ts +++ b/packages/ckeditor5-clipboard/src/clipboard.ts @@ -37,4 +37,30 @@ export default class Clipboard extends Plugin { public static get requires() { return [ ClipboardPipeline, DragDrop, PastePlainText ] as const; } + + /** + * @inheritDoc + */ + public init(): void { + const editor = this.editor; + const t = this.editor.t; + + // Add the information about the keystrokes to the accessibility database. + editor.accessibility.addKeystrokeInfos( { + keystrokes: [ + { + label: t( 'Copy selected content' ), + keystroke: 'CTRL+C' + }, + { + label: t( 'Paste content' ), + keystroke: 'CTRL+V' + }, + { + label: t( 'Paste content as plain text' ), + keystroke: 'CTRL+SHIFT+V' + } + ] + } ); + } } diff --git a/packages/ckeditor5-clipboard/tests/clipboard.js b/packages/ckeditor5-clipboard/tests/clipboard.js index 71424a0788b..ef85d0e5163 100644 --- a/packages/ckeditor5-clipboard/tests/clipboard.js +++ b/packages/ckeditor5-clipboard/tests/clipboard.js @@ -7,8 +7,28 @@ import Clipboard from '../src/clipboard.js'; import ClipboardPipeline from '../src/clipboardpipeline.js'; import DragDrop from '../src/dragdrop.js'; import PastePlainText from '../src/pasteplaintext.js'; +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor.js'; +import { global } from '@ckeditor/ckeditor5-utils'; describe( 'Clipboard Feature', () => { + let editor, domElement; + + beforeEach( async () => { + domElement = global.document.createElement( 'div' ); + global.document.body.appendChild( domElement ); + + editor = await ClassicTestEditor.create( domElement, { + plugins: [ + Clipboard + ] + } ); + } ); + + afterEach( async () => { + domElement.remove(); + await editor.destroy(); + } ); + it( 'requires ClipboardPipeline, DragDrop and PastePlainText', () => { expect( Clipboard.requires ).to.deep.equal( [ ClipboardPipeline, DragDrop, PastePlainText ] ); } ); @@ -16,4 +36,21 @@ describe( 'Clipboard Feature', () => { it( 'has proper name', () => { expect( Clipboard.pluginName ).to.equal( 'Clipboard' ); } ); + + it( 'should provide keystroke accessibility info', () => { + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'common' ).keystrokes ).to.deep.include( { + label: 'Copy selected content', + keystroke: 'CTRL+C' + } ); + + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'common' ).keystrokes ).to.deep.include( { + label: 'Paste content', + keystroke: 'CTRL+V' + } ); + + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'common' ).keystrokes ).to.deep.include( { + label: 'Paste content as plain text', + keystroke: 'CTRL+SHIFT+V' + } ); + } ); } ); diff --git a/packages/ckeditor5-core/lang/contexts.json b/packages/ckeditor5-core/lang/contexts.json index f69cab2a449..30809fc0135 100644 --- a/packages/ckeditor5-core/lang/contexts.json +++ b/packages/ckeditor5-core/lang/contexts.json @@ -11,5 +11,17 @@ "Insert with file manager": "The label for the insert image with the file manager toolbar button with visible label in insert image dropdown.", "Replace with file manager": "The label for the replace image with the file manager toolbar button with visible label in insert image dropdown.", "Insert image with file manager": "The label for the insert image with the file manager toolbar button.", - "Replace image with file manager": "The label for the replace image with the file manager toolbar button." + "Replace image with file manager": "The label for the replace image with the file manager toolbar button.", + "Toggle caption off": "The button label for the object (e.g. image, table) toolbar for hiding the attached caption.", + "Toggle caption on": "The button label for the object (e.g. image, table) toolbar for showing the attached caption.", + "Content editing keystrokes": "Accessibility help dialog category header text for keystrokes related to content creation.", + "These keyboard shortcuts allow for quick access to content editing features.": "Accessibility help dialog text further explaining the purpose of the \"Content editing keystrokes\" category.", + "User interface and content navigation keystrokes": "Accessibility help dialog category header text for keystrokes related to navigation in the user interface.", + "Use the following keystrokes for more efficient navigation in the CKEditor 5 user interface.": "Accessibility help dialog text further explaining the purpose of the \"User interface and content navigation keystrokes\" category.", + "Close contextual balloons, dropdowns, and dialogs": "Keystroke description for assistive technologies: keystroke for closing contextual balloons, dropdowns, and dialogs.", + "Open the accessibility help dialog": "Keystroke description for assistive technologies: keystroke for opening the accessibility help dialog.", + "Move focus between form fields (inputs, buttons, etc.)": "Keystroke description for assistive technologies: keystroke for moving between fields.", + "Move focus to the toolbar, navigate between toolbars": "Keystroke description for assistive technologies: keystroke for moving focus to the toolbar.", + "Navigate through the toolbar": "Keystroke description for assistive technologies: keystroke for navigating through the toolbar.", + "Execute the currently focused button": "Keystroke description for assistive technologies: keystroke for executing currently focused button." } diff --git a/packages/ckeditor5-core/src/accessibility.ts b/packages/ckeditor5-core/src/accessibility.ts new file mode 100644 index 00000000000..c8a08d33f96 --- /dev/null +++ b/packages/ckeditor5-core/src/accessibility.ts @@ -0,0 +1,525 @@ +/** + * @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 core/accessibility + */ + +import { CKEditorError } from '@ckeditor/ckeditor5-utils'; +import type Editor from './editor/editor.js'; + +const DEFAULT_CATEGORY_ID = 'contentEditing' as const; +export const DEFAULT_GROUP_ID = 'common' as const; + +/** + * A common namespace for various accessibility features of the editor. + * + * **Information about editor keystrokes** + * + * * The information about keystrokes available in the editor is stored in the {@link #keystrokeInfos} property. + * * New info entries can be added using the {@link #addKeystrokeInfoCategory}, {@link #addKeystrokeInfoGroup}, + * and {@link #addKeystrokeInfos} methods. + */ +export default class Accessibility { + /** + * Stores information about keystrokes brought by editor features for the users to interact with the editor, mainly + * keystroke combinations and their accessible labels. + * + * This information is particularly useful for screen reader and other assistive technology users. It gets displayed + * by the {@link module:ui/editorui/accessibilityhelp/accessibilityhelp~AccessibilityHelp Accessibility help} dialog. + * + * Keystrokes are organized in categories and groups. They can be added using ({@link #addKeystrokeInfoCategory}, + * {@link #addKeystrokeInfoGroup}, and {@link #addKeystrokeInfos}) methods. + * + * Please note that: + * * two categories are always available: + * * `'contentEditing'` for keystrokes related to content creation, + * * `'navigation'` for keystrokes related to navigation in the UI and the content. + * * unless specified otherwise, new keystrokes are added into the `'contentEditing'` category and the `'common'` + * keystroke group within that category while using the {@link #addKeystrokeInfos} method. + */ + public readonly keystrokeInfos: KeystrokeInfos = new Map(); + + /** + * The editor instance. + */ + private readonly _editor: Editor; + + /** + * @inheritDoc + */ + constructor( editor: Editor ) { + this._editor = editor; + + const t = editor.locale.t; + + this.addKeystrokeInfoCategory( { + id: DEFAULT_CATEGORY_ID, + label: t( 'Content editing keystrokes' ), + description: t( 'These keyboard shortcuts allow for quick access to content editing features.' ) + } ); + + this.addKeystrokeInfoCategory( { + id: 'navigation', + label: t( 'User interface and content navigation keystrokes' ), + description: t( 'Use the following keystrokes for more efficient navigation in the CKEditor 5 user interface.' ), + groups: [ + { + id: 'common', + keystrokes: [ + { + label: t( 'Close contextual balloons, dropdowns, and dialogs' ), + keystroke: 'Esc' + }, + { + label: t( 'Open the accessibility help dialog' ), + keystroke: 'Alt+0' + }, + { + label: t( 'Move focus between form fields (inputs, buttons, etc.)' ), + keystroke: [ [ 'Tab' ], [ 'Shift+Tab' ] ] + }, + { + label: t( 'Move focus to the toolbar, navigate between toolbars' ), + keystroke: 'Alt+F10', + mayRequireFn: true + }, + { + label: t( 'Navigate through the toolbar' ), + keystroke: [ [ 'arrowup' ], [ 'arrowright' ], [ 'arrowdown' ], [ 'arrowleft' ] ] + }, + { + label: t( 'Execute the currently focused button' ), + keystroke: [ [ 'Enter' ], [ 'Space' ] ] + } + ] + } + ] + } ); + } + + /** + * Adds a top-level category in the {@link #keystrokeInfos keystroke information database} with a label and optional description. + * + * Categories organize keystrokes and help users to find the right keystroke. Each category can have multiple groups + * of keystrokes that narrow down the context in which the keystrokes are available. Every keystroke category comes + * with a `'common'` group by default. + * + * By default, two categories are available: + * * `'contentEditing'` for keystrokes related to content creation, + * * `'navigation'` for keystrokes related to navigation in the UI and the content. + * + * To create a new keystroke category with new groups, use the following code: + * + * ```js + * class MyPlugin extends Plugin { + * // ... + * init() { + * const editor = this.editor; + * const t = editor.t; + * + * // ... + * + * editor.accessibility.addKeystrokeInfoCategory( { + * id: 'myCategory', + * label: t( 'My category' ), + * description: t( 'My category description.' ), + * groups: [ + * { + * id: 'myGroup', + * label: t( 'My keystroke group' ), + * keystrokes: [ + * { + * label: t( 'Keystroke label 1' ), + * keystroke: 'Ctrl+Shift+N' + * }, + * { + * label: t( 'Keystroke label 2' ), + * keystroke: 'Ctrl+Shift+M' + * } + * ] + * } + * ] + * }; + * } + * } + * ``` + * + * See {@link #keystrokeInfos}, {@link #addKeystrokeInfoGroup}, and {@link #addKeystrokeInfos}. + */ + public addKeystrokeInfoCategory( { id, label, description, groups }: AddKeystrokeInfoCategoryData ): void { + this.keystrokeInfos.set( id, { + id, + label, + description, + groups: new Map() + } ); + + this.addKeystrokeInfoGroup( { + categoryId: id, + id: DEFAULT_GROUP_ID + } ); + + if ( groups ) { + groups.forEach( group => { + this.addKeystrokeInfoGroup( { + categoryId: id, + ...group + } ); + } ); + } + } + + /** + * Adds a group of keystrokes in a specific category to the {@link #keystrokeInfos keystroke information database}. + * + * Groups narrow down the context in which the keystrokes are available. When `categoryId` is not specified, + * the group goes to the `'contentEditing'` category (default). + * + * To create a new group within an existing category, use the following code: + * + * ```js + * class MyPlugin extends Plugin { + * // ... + * init() { + * const editor = this.editor; + * const t = editor.t; + * + * // ... + * + * editor.accessibility.addKeystrokeInfoGroup( { + * id: 'myGroup', + * categoryId: 'navigation', + * label: t( 'My keystroke group' ), + * keystrokes: [ + * { + * label: t( 'Keystroke label 1' ), + * keystroke: 'Ctrl+Shift+N' + * }, + * { + * label: t( 'Keystroke label 2' ), + * keystroke: 'Ctrl+Shift+M' + * } + * ] + * } ); + * } + * } + * ``` + * + * See {@link #keystrokeInfos}, {@link #addKeystrokeInfoCategory}, and {@link #addKeystrokeInfos}. + */ + public addKeystrokeInfoGroup( { + categoryId = DEFAULT_CATEGORY_ID, + id, + label, + keystrokes + }: AddKeystrokeInfoGroupData ): void { + const category = this.keystrokeInfos.get( categoryId ); + + if ( !category ) { + throw new CKEditorError( 'accessibility-unknown-keystroke-info-category', this._editor, { groupId: id, categoryId } ); + } + + category.groups.set( id, { + id, + label, + keystrokes: keystrokes || [] + } ); + } + + /** + * Adds information about keystrokes to the {@link #keystrokeInfos keystroke information database}. + * + * Keystrokes without specified `groupId` or `categoryId` go to the `'common'` group in the `'contentEditing'` category (default). + * + * To add a keystroke brought by your plugin (using default group and category), use the following code: + * + * ```js + * class MyPlugin extends Plugin { + * // ... + * init() { + * const editor = this.editor; + * const t = editor.t; + * + * // ... + * + * editor.accessibility.addKeystrokeInfos( { + * keystrokes: [ + * { + * label: t( 'Keystroke label' ), + * keystroke: 'CTRL+B' + * } + * ] + * } ); + * } + * } + * ``` + * To add a keystroke in a specific existing `'widget'` group in the default `'contentEditing'` category: + * + * ```js + * class MyPlugin extends Plugin { + * // ... + * init() { + * const editor = this.editor; + * const t = editor.t; + * + * // ... + * + * editor.accessibility.addKeystrokeInfos( { + * // Add a keystroke to the existing "widget" group. + * groupId: 'widget', + * keystrokes: [ + * { + * label: t( 'A an action on a selected widget' ), + * keystroke: 'Ctrl+D', + * } + * ] + * } ); + * } + * } + * ``` + * + * To add a keystroke to another existing category (using default group): + * + * ```js + * class MyPlugin extends Plugin { + * // ... + * init() { + * const editor = this.editor; + * const t = editor.t; + * + * // ... + * + * editor.accessibility.addKeystrokeInfos( { + * // Add keystrokes to the "navigation" category (one of defaults). + * categoryId: 'navigation', + * keystrokes: [ + * { + * label: t( 'Keystroke label' ), + * keystroke: 'CTRL+B' + * } + * ] + * } ); + * } + * } + * ``` + * + * See {@link #keystrokeInfos}, {@link #addKeystrokeInfoGroup}, and {@link #addKeystrokeInfoCategory}. + */ + public addKeystrokeInfos( { + categoryId = DEFAULT_CATEGORY_ID, + groupId = DEFAULT_GROUP_ID, + keystrokes + }: AddKeystrokeInfosData ): void { + if ( !this.keystrokeInfos.has( categoryId ) ) { + /** + * Cannot add keystrokes in an unknown category. Use + * {@link module:core/accessibility~Accessibility#addKeystrokeInfoCategory} + * to add a new category or make sure the specified category exists. + * + * @error accessibility-unknown-keystroke-info-category + * @param categoryId The id of the unknown keystroke category. + * @param keystrokes Keystroke definitions about to be added. + */ + throw new CKEditorError( 'accessibility-unknown-keystroke-info-category', this._editor, { categoryId, keystrokes } ); + } + + const category = this.keystrokeInfos.get( categoryId )!; + + if ( !category.groups.has( groupId ) ) { + /** + * Cannot add keystrokes to an unknown group. + * + * Use {@link module:core/accessibility~Accessibility#addKeystrokeInfoGroup} + * to add a new group or make sure the specified group exists. + * + * @error accessibility-unknown-keystroke-info-group + * @param groupId The id of the unknown keystroke group. + * @param categoryId The id of category the unknown group should belong to. + * @param keystrokes Keystroke definitions about to be added. + */ + throw new CKEditorError( 'accessibility-unknown-keystroke-info-group', this._editor, { groupId, categoryId, keystrokes } ); + } + + category.groups.get( groupId )!.keystrokes.push( ...keystrokes ); + } +} + +/** + * A description of category of keystrokes accepted by the {@link module:core/accessibility~Accessibility#addKeystrokeInfoCategory} method. + * + * Top-level categories organize keystrokes and help users to find the right keystroke. Each category can have multiple groups of + * keystrokes that narrow down the context in which the keystrokes are available. + * + * See {@link module:core/accessibility~Accessibility#addKeystrokeInfoGroup} and + * {@link module:core/accessibility~Accessibility#addKeystrokeInfos}. + */ +export interface AddKeystrokeInfoCategoryData { + + /** + * The unique id of the category. + */ + id: string; + + /** + * The label of the category. + */ + label: string; + + /** + * The description of the category (optional). + */ + description?: string; + + /** + * Groups of keystrokes within the category. + */ + groups?: Array; +} + +/** + * A description of keystroke group accepted by the {@link module:core/accessibility~Accessibility#addKeystrokeInfoGroup} method. + * + * Groups narrow down the context in which the keystrokes are available. When `categoryId` is not specified, the group goes + * to the `'contentEditing'` category (default). + * + * See {@link module:core/accessibility~Accessibility#addKeystrokeInfoCategory} and + * {@link module:core/accessibility~Accessibility#addKeystrokeInfos}. + */ +export interface AddKeystrokeInfoGroupData { + + /** + * The category id the group belongs to. + */ + categoryId?: string; + + /** + * The unique id of the group. + */ + id: string; + + /** + * The label of the group (optional). + */ + label?: string; + + /** + * Keystroke definitions within the group. + */ + keystrokes?: Array; +} + +/** + * Description of keystrokes accepted by the {@link module:core/accessibility~Accessibility#addKeystrokeInfos} method. + * + * Keystrokes without specified `groupId` or `categoryId` go to the `'common'` group in the `'contentEditing'` category (default). + * + * See {@link module:core/accessibility~Accessibility#addKeystrokeInfoCategory} and + * {@link module:core/accessibility~Accessibility#addKeystrokeInfoGroup}. + */ +export interface AddKeystrokeInfosData { + + /** + * The category id the keystrokes belong to. + */ + categoryId?: string; + + /** + * The group id the keystrokes belong to. + */ + groupId?: string; + + /** + * An array of keystroke definitions. + */ + keystrokes: Array; +} + +export type KeystrokeInfos = Map; + +/** + * A category of keystrokes in {@link module:core/accessibility~Accessibility#keystrokeInfos}. + */ +export type KeystrokeInfoCategory = { + + /** + * The unique id of the category. + */ + id: string; + + /** + * The label of the category. + */ + label: string; + + /** + * The description of the category (optional). + */ + description?: string; + + /** + * Groups of keystrokes within the category. + */ + groups: Map; +}; + +/** + * A group of keystrokes in {@link module:core/accessibility~Accessibility#keystrokeInfos}. + */ +export type KeystrokeInfoGroup = { + + /** + * The unique id of the group. + */ + id: string; + + /** + * The label of the group (optional). + */ + label?: string; + + /** + * Keystroke definitions within the group. + */ + keystrokes: Array; +}; + +/** + * A keystroke info definition in {@link module:core/accessibility~Accessibility#keystrokeInfos} + */ +export interface KeystrokeInfoDefinition { + + /** + * The label of the keystroke. It should briefly describe the action that the keystroke performs. It may contain HTML. + */ + label: string; + + /** + * The keystroke string. In its basic form, it must be a combination of {@link module:utils/keyboard#keyCodes known key names} + * joined by the `+` sign, the same as the keystroke format accepted by the + * {@link module:utils/keystrokehandler~KeystrokeHandler#set `KeystrokeHandler#set()`} method used to register most of the + * keystroke interactions in the editor. + * + * * The keystroke string can represent a single keystroke, for instance: `keystroke: 'Ctrl+B'`, `keystroke: 'Shift+Enter'`, + * `keystroke: 'Alt+F10'`, etc. + * * The keystroke can be activated by successive press of multiple keys. For instance `keystroke: [ [ 'arrowleft', 'arrowleft' ] ]` + * will indicate that a specific action will be performed by pressing twice in a row. + * * Keystrokes can have alternatives. For instance `keystroke: [ [ 'Ctrl+Y' ], [ 'Ctrl+Shift+Z' ] ]` will indicate that + * a specific action can be performed by pressing either Ctrl + Y or + * Ctrl + Shift + Z. + * + * Please note that the keystrokes are automatically translated to the environment-specific form. For example, `Ctrl+A` + * will be rendered as `⌘A` in the {@link module:utils/env~EnvType#isMac Mac environment}. Always use the IBM PC keyboard + * syntax, for instance `Ctrl` instead of `⌘`, `Alt` instead of `⌥`, etc. + */ + keystroke: string | Array | Array>; + + /** + * This (optional) flag suggests that the keystroke(s) may require a function (Fn) key to be pressed + * in order to work in the {@link module:utils/env~EnvType#isMac Mac environment}. If set `true`, an additional + * information will be displayed next to the keystroke. + */ + mayRequireFn?: boolean; +} diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index ba062079931..2b914f6bcd5 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -30,6 +30,7 @@ import Context from '../context.js'; import PluginCollection from '../plugincollection.js'; import CommandCollection, { type CommandsMap } from '../commandcollection.js'; import EditingKeystrokeHandler from '../editingkeystrokehandler.js'; +import Accessibility from '../accessibility.js'; import type { LoadedPlugins, PluginConstructor } from '../plugin.js'; import type { EditorConfig } from './editorconfig.js'; @@ -53,6 +54,11 @@ import type { EditorConfig } from './editorconfig.js'; * (as most editor implementations do). */ export default abstract class Editor extends ObservableMixin() { + /** + * A namespace for the accessibility features of the editor. + */ + public readonly accessibility: Accessibility; + /** * Commands registered to the editor. * @@ -275,12 +281,13 @@ export default abstract class Editor extends ObservableMixin() { const constructor = this.constructor as typeof Editor; - // Prefer the language passed as the argument to the constructor instead of the constructor's `defaultConfig`, if both are set. - const language = config.language || ( constructor.defaultConfig && constructor.defaultConfig.language ); - // We don't pass translations to the config, because its behavior of splitting keys // with dots (e.g. `resize.width` => `resize: { width }`) breaks the translations. - const { translations, ...rest } = config; + const { translations: defaultTranslations, ...defaultConfig } = constructor.defaultConfig || {}; + const { translations = defaultTranslations, ...rest } = config; + + // Prefer the language passed as the argument to the constructor instead of the constructor's `defaultConfig`, if both are set. + const language = config.language || defaultConfig.language; this._context = config.context || new Context( { language, translations } ); this._context._addEditor( this, !config.context ); @@ -289,7 +296,7 @@ export default abstract class Editor extends ObservableMixin() { // between editors and make the watchdog feature work correctly. const availablePlugins = Array.from( constructor.builtinPlugins || [] ); - this.config = new Config( rest, constructor.defaultConfig ); + this.config = new Config( rest, defaultConfig ); this.config.define( 'plugins', availablePlugins ); this.config.define( this._context._getEditorConfig() ); @@ -325,6 +332,8 @@ export default abstract class Editor extends ObservableMixin() { this.keystrokes = new EditingKeystrokeHandler( this ); this.keystrokes.listenTo( this.editing.view.document ); + + this.accessibility = new Accessibility( this ); } /** diff --git a/packages/ckeditor5-core/src/index.ts b/packages/ckeditor5-core/src/index.ts index 97a5ef8cf46..9f365cf8925 100644 --- a/packages/ckeditor5-core/src/index.ts +++ b/packages/ckeditor5-core/src/index.ts @@ -33,6 +33,13 @@ export { default as secureSourceElement } from './editor/utils/securesourceeleme export { default as PendingActions, type PendingAction } from './pendingactions.js'; +export type { + KeystrokeInfos as KeystrokeInfoDefinitions, + KeystrokeInfoGroup as KeystrokeInfoGroupDefinition, + KeystrokeInfoCategory as KeystrokeInfoCategoryDefinition, + KeystrokeInfoDefinition as KeystrokeInfoDefinition +} from './accessibility.js'; + import cancel from './../theme/icons/cancel.svg'; import caption from './../theme/icons/caption.svg'; import check from './../theme/icons/check.svg'; diff --git a/packages/ckeditor5-core/tests/accessibility.js b/packages/ckeditor5-core/tests/accessibility.js new file mode 100644 index 00000000000..068c51b8a3c --- /dev/null +++ b/packages/ckeditor5-core/tests/accessibility.js @@ -0,0 +1,443 @@ +/** + * @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 { Editor } from '@ckeditor/ckeditor5-core'; +import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils.js'; +import { cloneDeep } from 'lodash-es'; + +describe( 'Accessibility', () => { + let editor, accessibility; + + beforeEach( () => { + editor = new Editor(); + accessibility = editor.accessibility; + } ); + + afterEach( async () => { + editor.destroy(); + } ); + + it( 'should provide default categories, groups, and keystrokes', () => { + expect( serializeKeystrokes( accessibility.keystrokeInfos ) ).to.deep.equal( [ + [ + 'contentEditing', + { + description: 'These keyboard shortcuts allow for quick access to content editing features.', + groups: [ + [ + 'common', + { + id: 'common', + keystrokes: [], + label: undefined + } + ] + ], + id: 'contentEditing', + label: 'Content editing keystrokes' + } + ], + [ + 'navigation', + { + description: 'Use the following keystrokes for more efficient navigation in the CKEditor 5 user interface.', + groups: [ + [ + 'common', + { + id: 'common', + keystrokes: [ + { + keystroke: 'Esc', + label: 'Close contextual balloons, dropdowns, and dialogs' + }, + { + keystroke: 'Alt+0', + label: 'Open the accessibility help dialog' + }, + { + keystroke: [ [ 'Tab' ], [ 'Shift+Tab' ] ], + label: 'Move focus between form fields (inputs, buttons, etc.)' + }, + { + keystroke: 'Alt+F10', + label: 'Move focus to the toolbar, navigate between toolbars', + mayRequireFn: true + }, + { + keystroke: [ + [ 'arrowup' ], + [ 'arrowright' ], + [ 'arrowdown' ], + [ 'arrowleft' ] + ], + label: 'Navigate through the toolbar' + }, + { + keystroke: [ + [ 'Enter' ], + [ 'Space' ] + ], + label: 'Execute the currently focused button' + } + ], + label: undefined + } + ] + ], + id: 'navigation', + label: 'User interface and content navigation keystrokes' + } + ] + ] ); + } ); + + describe( 'addKeystrokeInfoCategory()', () => { + it( 'should add a new category', () => { + accessibility.addKeystrokeInfoCategory( { + id: 'test', + label: 'Test category', + description: 'Test category description' + } ); + + const keystrokes = serializeKeystrokes( accessibility.keystrokeInfos ); + + expect( keystrokes ).to.deep.include( [ + 'test', + { + id: 'test', + label: 'Test category', + description: 'Test category description', + groups: [ + [ + 'common', + { + id: 'common', + label: undefined, + keystrokes: [] + } + ] + ] + } + ] ); + } ); + + it( 'should add child groups with keystrokes when specified', () => { + accessibility.addKeystrokeInfoCategory( { + id: 'testcat', + label: 'Test category', + description: 'Test category description', + groups: [ + { + id: 'testgroup', + label: 'Test group', + keystrokes: [ + { + label: 'Foo', + keystroke: 'Alt+C' + } + ] + } + ] + } ); + + expect( serializeKeystrokes( accessibility.keystrokeInfos ) ).to.deep.include( [ + 'testcat', + { + id: 'testcat', + label: 'Test category', + description: 'Test category description', + groups: [ + [ + 'common', + { + id: 'common', + label: undefined, + keystrokes: [] + } + ], + [ + 'testgroup', + { + id: 'testgroup', + label: 'Test group', + keystrokes: [ + { + keystroke: 'Alt+C', + label: 'Foo' + } + ] + } + ] + ] + } + ] ); + } ); + } ); + + describe( 'addKeystrokeInfoGroup()', () => { + it( 'should add a new group in the default category', () => { + accessibility.addKeystrokeInfoGroup( { + id: 'testgroup', + label: 'Test group', + keystrokes: [ + { + keystroke: 'Alt+C', + label: 'Foo' + } + ] + } ); + + expect( serializeKeystrokes( accessibility.keystrokeInfos ) ).to.deep.include( [ + 'contentEditing', + { + id: 'contentEditing', + label: 'Content editing keystrokes', + description: 'These keyboard shortcuts allow for quick access to content editing features.', + groups: [ + [ + 'common', + { + id: 'common', + label: undefined, + keystrokes: [] + } + ], + [ + 'testgroup', + { + id: 'testgroup', + label: 'Test group', + keystrokes: [ + { + keystroke: 'Alt+C', + label: 'Foo' + } + ] + } + ] + ] + } + ] ); + } ); + + it( 'should throw if the category was not found', () => { + expectToThrowCKEditorError( () => { + accessibility.addKeystrokeInfoGroup( { + id: 'testgroup', + categoryId: 'unknown-category', + label: 'Test group', + keystrokes: [ + { + keystroke: 'Alt+C', + label: 'Foo' + } + ] + } ); + }, /^accessibility-unknown-keystroke-info-category/, editor, { groupId: 'testgroup', categoryId: 'unknown-category' } ); + } ); + + it( 'should add group to a specific category', () => { + accessibility.addKeystrokeInfoCategory( { + id: 'testcat', + label: 'Test category', + description: 'Test category description' + } ); + + accessibility.addKeystrokeInfoGroup( { + id: 'testgroup', + categoryId: 'testcat', + label: 'Test group', + keystrokes: [ + { + keystroke: 'Alt+C', + label: 'Foo' + } + ] + } ); + + expect( serializeKeystrokes( accessibility.keystrokeInfos ) ).to.deep.include( [ + 'testcat', + { + id: 'testcat', + label: 'Test category', + description: 'Test category description', + groups: [ + [ + 'common', + { + id: 'common', + label: undefined, + keystrokes: [] + } + ], + [ + 'testgroup', + { + id: 'testgroup', + label: 'Test group', + keystrokes: [ + { + keystroke: 'Alt+C', + label: 'Foo' + } + ] + } + ] + ] + } + ] ); + } ); + } ); + + describe( 'addKeystrokeInfos()', () => { + it( 'should throw if the category does not exist', () => { + expectToThrowCKEditorError( () => { + accessibility.addKeystrokeInfos( { + categoryId: 'unknown-category', + keystrokes: [ + { + keystroke: 'Alt+C', + label: 'Foo' + } + ] + } ); + }, /^accessibility-unknown-keystroke-info-category/, editor, { + categoryId: 'unknown-category', + keystrokes: [ + { + keystroke: 'Alt+C', + label: 'Foo' + } + ] + } ); + } ); + + it( 'should throw if the group does not exist', () => { + expectToThrowCKEditorError( () => { + accessibility.addKeystrokeInfos( { + groupId: 'unknown-group', + keystrokes: [ + { + keystroke: 'Alt+C', + label: 'Foo' + } + ] + } ); + }, /^accessibility-unknown-keystroke-info-group/, editor, { + categoryId: 'contentEditing', + groupId: 'unknown-group', + keystrokes: [ + { + keystroke: 'Alt+C', + label: 'Foo' + } + ] + } ); + } ); + + it( 'should add keystrokes to a specific group in a specific category', () => { + accessibility.addKeystrokeInfoCategory( { + id: 'testcat', + label: 'Test category', + description: 'Test category description' + } ); + + accessibility.addKeystrokeInfoGroup( { + id: 'testgroup', + categoryId: 'testcat', + label: 'Test group' + } ); + + accessibility.addKeystrokeInfos( { + categoryId: 'testcat', + groupId: 'testgroup', + keystrokes: [ + { + keystroke: 'Alt+C', + label: 'Foo' + } + ] + } ); + + expect( serializeKeystrokes( accessibility.keystrokeInfos ) ).to.deep.include( [ + 'testcat', + { + id: 'testcat', + label: 'Test category', + description: 'Test category description', + groups: [ + [ + 'common', + { + id: 'common', + label: undefined, + keystrokes: [] + } + ], + [ + 'testgroup', + { + id: 'testgroup', + label: 'Test group', + keystrokes: [ + { + keystroke: 'Alt+C', + label: 'Foo' + } + ] + } + ] + ] + } + ] ); + } ); + + it( 'should add keystrokes to the default group and category ', () => { + accessibility.addKeystrokeInfos( { + keystrokes: [ + { + keystroke: 'Alt+C', + label: 'Foo' + } + ] + } ); + + expect( serializeKeystrokes( accessibility.keystrokeInfos ) ).to.deep.include( [ + 'contentEditing', + { + description: 'These keyboard shortcuts allow for quick access to content editing features.', + groups: [ + [ + 'common', + { + id: 'common', + keystrokes: [ + { + keystroke: 'Alt+C', + label: 'Foo' + } + ], + label: undefined + } + ] + ], + id: 'contentEditing', + label: 'Content editing keystrokes' + } + ] ); + } ); + } ); + + function serializeKeystrokes( keystrokes ) { + const serialized = Array.from( cloneDeep( keystrokes ).entries() ); + + for ( const [ , categoryDef ] of serialized ) { + categoryDef.groups = Array.from( categoryDef.groups.entries() ); + } + + return serialized; + } +} ); diff --git a/packages/ckeditor5-core/tests/context.js b/packages/ckeditor5-core/tests/context.js index acec22c32ed..647a84bb36c 100644 --- a/packages/ckeditor5-core/tests/context.js +++ b/packages/ckeditor5-core/tests/context.js @@ -505,7 +505,7 @@ describe( 'Context', () => { } ); } ); - describe( 'translations', () => { + describe( 'config.translations', () => { let editor, element; beforeEach( () => { @@ -546,6 +546,52 @@ describe( 'Context', () => { expect( editor.locale.translations.pl.dictionary[ 'a.b' ] ).to.equal( 'value' ); } ); } ); + + describe( 'defaultConfig.translations', () => { + let editor, element; + + beforeEach( () => { + element = document.createElement( 'div' ); + document.body.appendChild( element ); + + class TestEditor extends ClassicTestEditor {} + + TestEditor.defaultConfig = { + translations: { + pl: { + dictionary: { + bold: 'Pogrubienie', + 'a.b': 'value' + } + } + } + }; + + return TestEditor + .create( element ) + .then( _editor => { + editor = _editor; + } ); + } ); + + afterEach( () => { + document.body.removeChild( element ); + + return editor.destroy(); + } ); + + it( 'should not set translations in the config', () => { + expect( editor.config.get( 'translations' ) ).to.equal( undefined ); + } ); + + it( 'should properly get translations with the key', () => { + expect( editor.locale.translations.pl.dictionary.bold ).to.equal( 'Pogrubienie' ); + } ); + + it( 'should properly get translations with dot in the key', () => { + expect( editor.locale.translations.pl.dictionary[ 'a.b' ] ).to.equal( 'value' ); + } ); + } ); } ); function getPlugins( editor ) { diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index e30000c02fb..4882f449c00 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -19,6 +19,7 @@ import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_uti import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror.js'; import testUtils from '../../tests/_utils/utils.js'; import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model.js'; +import Accessibility from '../../src/accessibility.js'; class TestEditor extends Editor { static create( config ) { @@ -130,6 +131,7 @@ describe( 'Editor', () => { it( 'should create a new editor instance', () => { const editor = new TestEditor(); + expect( editor.accessibility ).to.be.an.instanceof( Accessibility ); expect( editor.config ).to.be.an.instanceof( Config ); expect( editor.commands ).to.be.an.instanceof( CommandCollection ); expect( editor.editing ).to.be.instanceof( EditingController ); @@ -177,6 +179,22 @@ describe( 'Editor', () => { expect( editor.config.get( 'translations' ) ).to.equal( undefined ); } ); + it( 'should use translations set as the defaultConfig option on the constructor', () => { + TestEditor.defaultConfig = { + translations: { + pl: { + dictionary: { + Bold: 'Pogrubienie' + } + } + } + }; + + const editor = new TestEditor(); + + expect( editor.config.get( 'translations' ) ).to.equal( undefined ); + } ); + it( 'should bind editing.view.document#isReadOnly to the editor#isReadOnly', () => { const editor = new TestEditor(); diff --git a/packages/ckeditor5-editor-multi-root/src/index.ts b/packages/ckeditor5-editor-multi-root/src/index.ts index 3f17c8657f4..22d98f8d8f0 100644 --- a/packages/ckeditor5-editor-multi-root/src/index.ts +++ b/packages/ckeditor5-editor-multi-root/src/index.ts @@ -9,6 +9,10 @@ export { default as MultiRootEditor } from './multirooteditor.js'; -export type { RootAttributes } from './multirooteditor.js'; +export type { + RootAttributes, + AddRootEvent, + DetachRootEvent +} from './multirooteditor.js'; import './augmentation.js'; diff --git a/packages/ckeditor5-engine/src/conversion/viewconsumable.ts b/packages/ckeditor5-engine/src/conversion/viewconsumable.ts index b4aab91b8ec..91436e98d09 100644 --- a/packages/ckeditor5-engine/src/conversion/viewconsumable.ts +++ b/packages/ckeditor5-engine/src/conversion/viewconsumable.ts @@ -7,7 +7,7 @@ * @module engine/conversion/viewconsumable */ -import { CKEditorError } from '@ckeditor/ckeditor5-utils'; +import { CKEditorError, toArray, type ArrayOrItem } from '@ckeditor/ckeditor5-utils'; import type Element from '../view/element.js'; import type Node from '../view/node.js'; @@ -15,8 +15,6 @@ import type Text from '../view/text.js'; import type DocumentFragment from '../view/documentfragment.js'; import type { Match } from '../view/matcher.js'; -import { isArray } from 'lodash-es'; - /** * Class used for handling consumption of view {@link module:engine/view/element~Element elements}, * {@link module:engine/view/text~Text text nodes} and {@link module:engine/view/documentfragment~DocumentFragment document fragments}. @@ -558,8 +556,8 @@ export class ViewElementConsumables { * @param type Type of the consumable item: `attributes`, `classes` or `styles`. * @param item Consumable item or array of items. */ - private _add( type: ConsumableType, item: string | Array ) { - const items = isArray( item ) ? item : [ item ]; + private _add( type: ConsumableType, item: ArrayOrItem ) { + const items = toArray( item ); const consumables = this._consumables[ type ]; for ( const name of items ) { @@ -603,8 +601,8 @@ export class ViewElementConsumables { * @returns Returns `true` if all items can be consumed, `null` when one of the items cannot be * consumed and `false` when one of the items is already consumed. */ - private _test( type: ConsumableType, item: string | Array ): boolean | null { - const items = isArray( item ) ? item : [ item ]; + private _test( type: ConsumableType, item: ArrayOrItem ): boolean | null { + const items = toArray( item ); const consumables = this._consumables[ type ]; for ( const name of items ) { @@ -639,8 +637,8 @@ export class ViewElementConsumables { * @param type Type of the consumable item: `attributes`, `classes` or `styles`. * @param item Consumable item or array of items. */ - private _consume( type: ConsumableType, item: string | Array ) { - const items = isArray( item ) ? item : [ item ]; + private _consume( type: ConsumableType, item: ArrayOrItem ) { + const items = toArray( item ); const consumables = this._consumables[ type ]; for ( const name of items ) { @@ -667,8 +665,8 @@ export class ViewElementConsumables { * @param type Type of the consumable item: `attributes`, `classes` or , `styles`. * @param item Consumable item or array of items. */ - private _revert( type: ConsumableType, item: string | Array ) { - const items = isArray( item ) ? item : [ item ]; + private _revert( type: ConsumableType, item: ArrayOrItem ) { + const items = toArray( item ); const consumables = this._consumables[ type ]; for ( const name of items ) { diff --git a/packages/ckeditor5-engine/src/index.ts b/packages/ckeditor5-engine/src/index.ts index 109bc0f1372..33215db0623 100644 --- a/packages/ckeditor5-engine/src/index.ts +++ b/packages/ckeditor5-engine/src/index.ts @@ -191,7 +191,7 @@ export type { ViewDocumentSelectionChangeEvent } from './view/observer/selection export type { ViewRenderEvent, ViewScrollToTheSelectionEvent } from './view/view.js'; // View / Styles. -export { StylesProcessor, type BoxSides } from './view/stylesmap.js'; +export { default as StylesMap, StylesProcessor, type BoxSides } from './view/stylesmap.js'; export * from './view/styles/background.js'; export * from './view/styles/border.js'; export * from './view/styles/margin.js'; diff --git a/packages/ckeditor5-engine/src/view/styles/padding.ts b/packages/ckeditor5-engine/src/view/styles/padding.ts index ce24cd2b475..667092ebb77 100644 --- a/packages/ckeditor5-engine/src/view/styles/padding.ts +++ b/packages/ckeditor5-engine/src/view/styles/padding.ts @@ -11,7 +11,7 @@ import type { StylesProcessor } from '../stylesmap.js'; import { getPositionShorthandNormalizer, getBoxSidesValueReducer } from './utils.js'; /** - * Adds a margin CSS styles processing rules. + * Adds a padding CSS styles processing rules. * * ```ts * editor.data.addStyleProcessorRules( addPaddingRules ); diff --git a/packages/ckeditor5-engine/src/view/stylesmap.ts b/packages/ckeditor5-engine/src/view/stylesmap.ts index 61ed4ab6203..7cb6859636e 100644 --- a/packages/ckeditor5-engine/src/view/stylesmap.ts +++ b/packages/ckeditor5-engine/src/view/stylesmap.ts @@ -11,8 +11,6 @@ import { get, isObject, merge, set, unset } from 'lodash-es'; /** * Styles map. Allows handling (adding, removing, retrieving) a set of style rules (usually, of an element). - * - * The styles map is capable of normalizing style names so e.g. the following operations are possible: */ export default class StylesMap { /** diff --git a/packages/ckeditor5-enter/lang/contexts.json b/packages/ckeditor5-enter/lang/contexts.json new file mode 100644 index 00000000000..5d051ce2f7f --- /dev/null +++ b/packages/ckeditor5-enter/lang/contexts.json @@ -0,0 +1,4 @@ +{ + "Insert a soft break (a <br> element)": "Keystroke description for assistive technologies: keystroke for inserting a soft break.", + "Insert a hard break (a new paragraph)": "Keystroke description for assistive technologies: keystroke for inserting a hard break." +} diff --git a/packages/ckeditor5-enter/src/enter.ts b/packages/ckeditor5-enter/src/enter.ts index f5d5e5c3e7e..bb668128060 100644 --- a/packages/ckeditor5-enter/src/enter.ts +++ b/packages/ckeditor5-enter/src/enter.ts @@ -30,6 +30,7 @@ export default class Enter extends Plugin { const editor = this.editor; const view = editor.editing.view; const viewDocument = view.document; + const t = this.editor.t; view.addObserver( EnterObserver ); @@ -51,5 +52,15 @@ export default class Enter extends Plugin { view.scrollToTheSelection(); }, { priority: 'low' } ); + + // Add the information about the keystroke to the accessibility database. + editor.accessibility.addKeystrokeInfos( { + keystrokes: [ + { + label: t( 'Insert a hard break (a new paragraph)' ), + keystroke: 'Enter' + } + ] + } ); } } diff --git a/packages/ckeditor5-enter/src/shiftenter.ts b/packages/ckeditor5-enter/src/shiftenter.ts index 011d47c9677..2c1d478f163 100644 --- a/packages/ckeditor5-enter/src/shiftenter.ts +++ b/packages/ckeditor5-enter/src/shiftenter.ts @@ -32,6 +32,7 @@ export default class ShiftEnter extends Plugin { const conversion = editor.conversion; const view = editor.editing.view; const viewDocument = view.document; + const t = this.editor.t; // Configure the schema. schema.register( 'softBreak', { @@ -71,5 +72,15 @@ export default class ShiftEnter extends Plugin { editor.execute( 'shiftEnter' ); view.scrollToTheSelection(); }, { priority: 'low' } ); + + // Add the information about the keystroke to the accessibility database. + editor.accessibility.addKeystrokeInfos( { + keystrokes: [ + { + label: t( 'Insert a soft break (a <br> element)' ), + keystroke: 'Shift+Enter' + } + ] + } ); } } diff --git a/packages/ckeditor5-enter/tests/enter.js b/packages/ckeditor5-enter/tests/enter.js index f907d4e8297..6480d996eb9 100644 --- a/packages/ckeditor5-enter/tests/enter.js +++ b/packages/ckeditor5-enter/tests/enter.js @@ -35,6 +35,17 @@ describe( 'Enter feature', () => { return editor.destroy(); } ); + it( 'should have pluginName', () => { + expect( Enter.pluginName ).to.equal( 'Enter' ); + } ); + + it( 'should add keystroke accessibility info', () => { + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'common' ).keystrokes ).to.deep.include( { + label: 'Insert a hard break (a new paragraph)', + keystroke: 'Enter' + } ); + } ); + it( 'creates the commands', () => { expect( editor.commands.get( 'enter' ) ).to.be.instanceof( EnterCommand ); } ); diff --git a/packages/ckeditor5-enter/tests/shiftenter.js b/packages/ckeditor5-enter/tests/shiftenter.js index e42da088651..55f3b7f9546 100644 --- a/packages/ckeditor5-enter/tests/shiftenter.js +++ b/packages/ckeditor5-enter/tests/shiftenter.js @@ -35,6 +35,17 @@ describe( 'ShiftEnter feature', () => { return editor.destroy(); } ); + it( 'should have pluginName', () => { + expect( ShiftEnter.pluginName ).to.equal( 'ShiftEnter' ); + } ); + + it( 'should add keystroke accessibility info', () => { + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'common' ).keystrokes ).to.deep.include( { + label: 'Insert a soft break (a <br> element)', + keystroke: 'Shift+Enter' + } ); + } ); + it( 'creates the commands', () => { expect( editor.commands.get( 'shiftEnter' ) ).to.be.instanceof( ShiftEnterCommand ); } ); diff --git a/packages/ckeditor5-essentials/package.json b/packages/ckeditor5-essentials/package.json index be51c464e3e..20984d93a2a 100644 --- a/packages/ckeditor5-essentials/package.json +++ b/packages/ckeditor5-essentials/package.json @@ -25,6 +25,7 @@ "@ckeditor/ckeditor5-select-all": "41.1.0", "@ckeditor/ckeditor5-theme-lark": "41.1.0", "@ckeditor/ckeditor5-typing": "41.1.0", + "@ckeditor/ckeditor5-ui": "41.1.0", "@ckeditor/ckeditor5-undo": "41.1.0", "typescript": "5.0.4", "webpack": "^5.58.1", diff --git a/packages/ckeditor5-essentials/src/essentials.ts b/packages/ckeditor5-essentials/src/essentials.ts index 64b0e7d961e..f6d93b04cbb 100644 --- a/packages/ckeditor5-essentials/src/essentials.ts +++ b/packages/ckeditor5-essentials/src/essentials.ts @@ -14,6 +14,7 @@ import { Enter, ShiftEnter } from 'ckeditor5/src/enter.js'; import { SelectAll } from 'ckeditor5/src/select-all.js'; import { Typing } from 'ckeditor5/src/typing.js'; import { Undo } from 'ckeditor5/src/undo.js'; +import { AccessibilityHelp } from 'ckeditor5/src/ui.js'; /** * A plugin including all essential editing features. It represents a set of features that enables similar functionalities @@ -36,7 +37,7 @@ export default class Essentials extends Plugin { * @inheritDoc */ public static get requires() { - return [ Clipboard, Enter, SelectAll, ShiftEnter, Typing, Undo ] as const; + return [ AccessibilityHelp, Clipboard, Enter, SelectAll, ShiftEnter, Typing, Undo ] as const; } /** diff --git a/packages/ckeditor5-essentials/tests/essentials.js b/packages/ckeditor5-essentials/tests/essentials.js index da826d8cefa..1ce0f5d72fe 100644 --- a/packages/ckeditor5-essentials/tests/essentials.js +++ b/packages/ckeditor5-essentials/tests/essentials.js @@ -14,6 +14,7 @@ import SelectAll from '@ckeditor/ckeditor5-select-all/src/selectall.js'; import ShiftEnter from '@ckeditor/ckeditor5-enter/src/shiftenter.js'; import Typing from '@ckeditor/ckeditor5-typing/src/typing.js'; import Undo from '@ckeditor/ckeditor5-undo/src/undo.js'; +import { AccessibilityHelp } from '@ckeditor/ckeditor5-ui'; describe( 'Essentials preset', () => { let editor, editorElement; @@ -39,6 +40,7 @@ describe( 'Essentials preset', () => { } ); it( 'should load all its dependencies', () => { + expect( editor.plugins.get( AccessibilityHelp ) ).to.be.instanceOf( AccessibilityHelp ); expect( editor.plugins.get( Clipboard ) ).to.be.instanceOf( Clipboard ); expect( editor.plugins.get( Enter ) ).to.be.instanceOf( Enter ); expect( editor.plugins.get( SelectAll ) ).to.be.instanceOf( SelectAll ); diff --git a/packages/ckeditor5-find-and-replace/lang/contexts.json b/packages/ckeditor5-find-and-replace/lang/contexts.json index a5a53472bc1..cbded05dcb9 100644 --- a/packages/ckeditor5-find-and-replace/lang/contexts.json +++ b/packages/ckeditor5-find-and-replace/lang/contexts.json @@ -11,5 +11,6 @@ "Replace with…": "The label for the text replacement in the find and replace dropdown.", "Text to find must not be empty.": "An error text displayed when user attempted to find an empty text.", "Tip: Find some text first in order to replace it.": "A message displayed next to the replace field when disabled but user tries to use it.", - "Advanced options": "The label and the tooltip of the options dropdown button in the find and replace form." + "Advanced options": "The label and the tooltip of the options dropdown button in the find and replace form.", + "Find in the document": "Keystroke description for assistive technologies: keystroke for opening the find and replace UI." } diff --git a/packages/ckeditor5-find-and-replace/src/findandreplaceui.ts b/packages/ckeditor5-find-and-replace/src/findandreplaceui.ts index bf1bd423c03..8412259b185 100644 --- a/packages/ckeditor5-find-and-replace/src/findandreplaceui.ts +++ b/packages/ckeditor5-find-and-replace/src/findandreplaceui.ts @@ -71,6 +71,7 @@ export default class FindAndReplaceUI extends Plugin { const editor = this.editor; const isUiUsingDropdown = editor.config.get( 'findAndReplace.uiType' ) === 'dropdown'; const findCommand = editor.commands.get( 'find' )!; + const t = this.editor.t; // Register the toolbar component: dropdown or button (that opens a dialog). editor.ui.componentFactory.add( 'findAndReplace', () => { @@ -122,6 +123,16 @@ export default class FindAndReplaceUI extends Plugin { return this._createDialogButtonForMenuBar(); } ); } + + // Add the information about the keystroke to the accessibility database. + editor.accessibility.addKeystrokeInfos( { + keystrokes: [ + { + label: t( 'Find in the document' ), + keystroke: 'CTRL+F' + } + ] + } ); } /** diff --git a/packages/ckeditor5-find-and-replace/tests/findandreplaceui.js b/packages/ckeditor5-find-and-replace/tests/findandreplaceui.js index feb22650e84..3bb3bc75b19 100644 --- a/packages/ckeditor5-find-and-replace/tests/findandreplaceui.js +++ b/packages/ckeditor5-find-and-replace/tests/findandreplaceui.js @@ -63,6 +63,13 @@ describe( 'FindAndReplaceUI', () => { return editor.destroy(); } ); + it( 'should add keystroke accessibility info', () => { + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'common' ).keystrokes ).to.deep.include( { + label: 'Find in the document', + keystroke: 'CTRL+F' + } ); + } ); + it( 'should register a button UI compontent', () => { expect( toolbarButtonView ).to.be.instanceOf( ButtonView ); } ); @@ -475,6 +482,13 @@ describe( 'FindAndReplaceUI', () => { return editor.destroy(); } ); + it( 'should add keystroke accessibility info', () => { + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'common' ).keystrokes ).to.deep.include( { + label: 'Find in the document', + keystroke: 'CTRL+F' + } ); + } ); + it( 'should create a dropdown UI component', () => { expect( dropdown ).to.be.instanceOf( DropdownView ); } ); diff --git a/packages/ckeditor5-horizontal-line/src/horizontallineui.ts b/packages/ckeditor5-horizontal-line/src/horizontallineui.ts index 26cfce1668b..7effba3f489 100644 --- a/packages/ckeditor5-horizontal-line/src/horizontallineui.ts +++ b/packages/ckeditor5-horizontal-line/src/horizontallineui.ts @@ -8,7 +8,7 @@ */ import { icons, Plugin } from 'ckeditor5/src/core.js'; -import { ButtonView } from 'ckeditor5/src/ui.js'; +import { ButtonView, MenuBarMenuListItemButtonView } from 'ckeditor5/src/ui.js'; import type HorizontalLineCommand from './horizontallinecommand.js'; @@ -28,28 +28,46 @@ export default class HorizontalLineUI extends Plugin { */ public init(): void { const editor = this.editor; - const t = editor.t; // Add the `horizontalLine` button to feature components. - editor.ui.componentFactory.add( 'horizontalLine', locale => { - const command: HorizontalLineCommand = editor.commands.get( 'horizontalLine' )!; - const view = new ButtonView( locale ); + editor.ui.componentFactory.add( 'horizontalLine', () => { + const buttonView = this._createButton( ButtonView ); - view.set( { - label: t( 'Horizontal line' ), - icon: icons.horizontalLine, + buttonView.set( { tooltip: true } ); - view.bind( 'isEnabled' ).to( command, 'isEnabled' ); + return buttonView; + } ); - // Execute the command. - this.listenTo( view, 'execute', () => { - editor.execute( 'horizontalLine' ); - editor.editing.view.focus(); - } ); + editor.ui.componentFactory.add( 'menuBar:horizontalLine', () => { + return this._createButton( MenuBarMenuListItemButtonView ); + } ); + } + + /** + * Creates a button for horizontal line command to use either in toolbar or in menu bar. + */ + private _createButton( ButtonClass: T ): InstanceType { + const editor = this.editor; + const locale = editor.locale; + const command: HorizontalLineCommand = editor.commands.get( 'horizontalLine' )!; + const view = new ButtonClass( editor.locale ) as InstanceType; + const t = locale.t; + + view.set( { + label: t( 'Horizontal line' ), + icon: icons.horizontalLine + } ); + + view.bind( 'isEnabled' ).to( command, 'isEnabled' ); - return view; + // Execute the command. + this.listenTo( view, 'execute', () => { + editor.execute( 'horizontalLine' ); + editor.editing.view.focus(); } ); + + return view; } } diff --git a/packages/ckeditor5-html-embed/lang/contexts.json b/packages/ckeditor5-html-embed/lang/contexts.json index 3227cf17be9..da97ca4cda9 100644 --- a/packages/ckeditor5-html-embed/lang/contexts.json +++ b/packages/ckeditor5-html-embed/lang/contexts.json @@ -1,5 +1,6 @@ { "Insert HTML": "Toolbar button tooltip for the HTML embed feature.", + "HTML": "Application menu bar button label for the HTML embed feature.", "HTML snippet": "The HTML snippet.", "Paste raw HTML here...": "A placeholder that will be displayed in the raw HTML textarea field.", "Edit source": "A label of a button that switches the HTML embed to the source editing mode.", diff --git a/packages/ckeditor5-html-embed/src/htmlembedui.ts b/packages/ckeditor5-html-embed/src/htmlembedui.ts index 8eada05a436..5fdc4fdbfcd 100644 --- a/packages/ckeditor5-html-embed/src/htmlembedui.ts +++ b/packages/ckeditor5-html-embed/src/htmlembedui.ts @@ -8,7 +8,7 @@ */ import { icons, Plugin } from 'ckeditor5/src/core.js'; -import { ButtonView } from 'ckeditor5/src/ui.js'; +import { ButtonView, MenuBarMenuListItemButtonView } from 'ckeditor5/src/ui.js'; import type { RawHtmlApi } from './htmlembedediting.js'; import type HtmlEmbedCommand from './htmlembedcommand.js'; @@ -28,34 +28,58 @@ export default class HtmlEmbedUI extends Plugin { */ public init(): void { const editor = this.editor; - const t = editor.t; + const locale = editor.locale; + const t = locale.t; // Add the `htmlEmbed` button to feature components. - editor.ui.componentFactory.add( 'htmlEmbed', locale => { - const command: HtmlEmbedCommand = editor.commands.get( 'htmlEmbed' )!; - const view = new ButtonView( locale ); - - view.set( { - label: t( 'Insert HTML' ), - icon: icons.html, - tooltip: true - } ); + editor.ui.componentFactory.add( 'htmlEmbed', () => { + const buttonView = this._createButton( ButtonView ); - view.bind( 'isEnabled' ).to( command, 'isEnabled' ); + buttonView.set( { + tooltip: true, + label: t( 'Insert HTML' ) + } ); - // Execute the command. - this.listenTo( view, 'execute', () => { - editor.execute( 'htmlEmbed' ); - editor.editing.view.focus(); + return buttonView; + } ); - const rawHtmlApi = editor.editing.view.document.selection - .getSelectedElement()! - .getCustomProperty( 'rawHtmlApi' ) as RawHtmlApi; + editor.ui.componentFactory.add( 'menuBar:htmlEmbed', () => { + const buttonView = this._createButton( MenuBarMenuListItemButtonView ); - rawHtmlApi.makeEditable(); + buttonView.set( { + label: t( 'HTML' ) } ); - return view; + return buttonView; + } ); + } + + /** + * Creates a button for html embed command to use either in toolbar or in menu bar. + */ + private _createButton( ButtonClass: T ): InstanceType { + const editor = this.editor; + const command: HtmlEmbedCommand = editor.commands.get( 'htmlEmbed' )!; + const view = new ButtonClass( editor.locale ) as InstanceType; + + view.set( { + icon: icons.html + } ); + + view.bind( 'isEnabled' ).to( command, 'isEnabled' ); + + // Execute the command. + this.listenTo( view, 'execute', () => { + editor.execute( 'htmlEmbed' ); + editor.editing.view.focus(); + + const rawHtmlApi = editor.editing.view.document.selection + .getSelectedElement()! + .getCustomProperty( 'rawHtmlApi' ) as RawHtmlApi; + + rawHtmlApi.makeEditable(); } ); + + return view; } } diff --git a/packages/ckeditor5-html-support/src/datafilter.ts b/packages/ckeditor5-html-support/src/datafilter.ts index 0b4b587b567..a5d0222ceb6 100644 --- a/packages/ckeditor5-html-support/src/datafilter.ts +++ b/packages/ckeditor5-html-support/src/datafilter.ts @@ -11,10 +11,10 @@ import { Plugin, type Editor } from 'ckeditor5/src/core.js'; import { Matcher, + StylesMap, type MatcherPattern, type UpcastConversionApi, type ViewElement, - type MatchResult, type ViewConsumable, type MatcherObjectPattern, type DocumentSelectionChangeAttributeEvent @@ -53,7 +53,7 @@ import { type GHSViewAttributes } from './utils.js'; -import { isPlainObject, pull as removeItemFromArray } from 'lodash-es'; +import { isPlainObject } from 'lodash-es'; import '../theme/datafilter.css'; @@ -311,11 +311,13 @@ export default class DataFilter extends Plugin { * - classes Set with matched class names. */ public processViewAttributes( viewElement: ViewElement, conversionApi: UpcastConversionApi ): GHSViewAttributes | null { + const { consumable } = conversionApi; + // Make sure that the disabled attributes are handled before the allowed attributes are called. // For example, for block images the
converter triggers conversion for first and then for other elements, i.e. . - consumeAttributes( viewElement, conversionApi, this._disallowedAttributes ); + matchAndConsumeAttributes( viewElement, this._disallowedAttributes, consumable ); - return consumeAttributes( viewElement, conversionApi, this._allowedAttributes ); + return prepareGHSAttribute( viewElement, matchAndConsumeAttributes( viewElement, this._allowedAttributes, consumable ) ); } /** @@ -805,129 +807,133 @@ export interface DataFilterRegisterEvent { } /** - * Matches and consumes the given view attributes. + * Matches and consumes matched attributes. + * + * @returns Object with following properties: + * - attributes Array with matched attribute names. + * - classes Array with matched class names. + * - styles Array with matched style names. */ -function consumeAttributes( viewElement: ViewElement, conversionApi: UpcastConversionApi, matcher: Matcher ) { - const matches = consumeAttributeMatches( viewElement, conversionApi, matcher ); - const { attributes, styles, classes } = mergeMatchResults( matches ); - const viewAttributes: GHSViewAttributes = {}; - - // Remove invalid DOM element attributes. - if ( attributes.size ) { - for ( const key of attributes ) { - if ( !isValidAttributeName( key as string ) ) { - attributes.delete( key ); +function matchAndConsumeAttributes( + viewElement: ViewElement, + matcher: Matcher, + consumable: ViewConsumable +): { + attributes: Array; + classes: Array; + styles: Array; +} { + const matches = matcher.matchAll( viewElement ) || []; + const stylesProcessor = viewElement.document.stylesProcessor; + + return matches.reduce( ( result, { match } ) => { + // Verify and consume styles. + for ( const style of match.styles || [] ) { + // Check longer forms of the same style as those could be matched + // but not present in the element directly. + // Consider only longhand (or longer than current notation) so that + // we do not include all sides of the box if only one side is allowed. + const sortedRelatedStyles = stylesProcessor.getRelatedStyles( style ) + .filter( relatedStyle => relatedStyle.split( '-' ).length > style.split( '-' ).length ) + .sort( ( a, b ) => b.split( '-' ).length - a.split( '-' ).length ); + + for ( const relatedStyle of sortedRelatedStyles ) { + if ( consumable.consume( viewElement, { styles: [ relatedStyle ] } ) ) { + result.styles.push( relatedStyle ); + } } - } - } - if ( attributes.size ) { - viewAttributes.attributes = iterableToObject( attributes, key => viewElement.getAttribute( key ) ); - } - - if ( styles.size ) { - viewAttributes.styles = iterableToObject( styles, key => viewElement.getStyle( key ) ); - } + // Verify and consume style as specified in the matcher. + if ( consumable.consume( viewElement, { styles: [ style ] } ) ) { + result.styles.push( style ); + } + } - if ( classes.size ) { - viewAttributes.classes = Array.from( classes ); - } + // Verify and consume class names. + for ( const className of match.classes || [] ) { + if ( consumable.consume( viewElement, { classes: [ className ] } ) ) { + result.classes.push( className ); + } + } - if ( !Object.keys( viewAttributes ).length ) { - return null; - } + // Verify and consume other attributes. + for ( const attributeName of match.attributes || [] ) { + if ( consumable.consume( viewElement, { attributes: [ attributeName ] } ) ) { + result.attributes.push( attributeName ); + } + } - return viewAttributes; + return result; + }, { + attributes: [] as Array, + classes: [] as Array, + styles: [] as Array + } ); } /** - * Consumes matched attributes. - * - * @returns Array with match information about found attributes. + * Prepares the GHS attribute value as an object with element attributes' values. */ -function consumeAttributeMatches( viewElement: ViewElement, { consumable }: UpcastConversionApi, matcher: Matcher ): Array { - const matches = matcher.matchAll( viewElement ) || []; - const consumedMatches = []; - - for ( const match of matches ) { - removeConsumedAttributes( consumable, viewElement, match ); - - // We only want to consume attributes, so element can be still processed by other converters. - delete match.match.name; - - consumable.consume( viewElement, match.match ); - consumedMatches.push( match ); +function prepareGHSAttribute( + viewElement: ViewElement, + { attributes, classes, styles }: { + attributes: Array; + classes: Array; + styles: Array; + } +): GHSViewAttributes | null { + if ( !attributes.length && !classes.length && !styles.length ) { + return null; } - return consumedMatches; -} + return { + ...( attributes.length && { + attributes: getAttributes( viewElement, attributes ) + } ), -/** - * Removes attributes from the given match that were already consumed by other converters. - */ -function removeConsumedAttributes( consumable: ViewConsumable, viewElement: ViewElement, match: MatchResult ) { - for ( const key of [ 'attributes', 'classes', 'styles' ] as const ) { - const attributes = match.match[ key ]; - - if ( !attributes ) { - continue; - } + ...( styles.length && { + styles: getReducedStyles( viewElement, styles ) + } ), - // Iterating over a copy of an array so removing items doesn't influence iteration. - for ( const value of Array.from( attributes ) ) { - if ( !consumable.test( viewElement, ( { [ key ]: [ value ] } ) ) ) { - removeItemFromArray( attributes, value ); - } - } - } + ...( classes.length && { + classes + } ) + }; } /** - * Merges the result of {@link module:engine/view/matcher~Matcher#matchAll} method. - * - * @param matches - * @returns Object with following properties: - * - attributes Set with matched attribute names. - * - styles Set with matched style names. - * - classes Set with matched class names. + * Returns attributes as an object with names and values. */ -function mergeMatchResults( matches: Array ): -{ - attributes: Set; - styles: Set; - classes: Set; -} { - const matchResult = { - attributes: new Set(), - classes: new Set(), - styles: new Set() - }; +function getAttributes( viewElement: ViewElement, attributes: Iterable ): Record { + const attributesObject: Record = {}; - for ( const match of matches ) { - for ( const key in matchResult ) { - const values: Array = match.match[ key as keyof typeof matchResult ] || []; + for ( const key of attributes ) { + const value = viewElement.getAttribute( key ); - values.forEach( value => ( matchResult[ key as keyof typeof matchResult ] ).add( value ) ); + if ( value !== undefined && isValidAttributeName( key ) ) { + attributesObject[ key ] = value; } } - return matchResult; + return attributesObject; } /** - * Converts the given iterable object into an object. + * Returns styles as an object reduced to shorthand notation without redundant entries. */ -function iterableToObject( iterable: Set, getValue: ( s: string ) => any ) { - const attributesObject: Record = {}; +function getReducedStyles( viewElement: ViewElement, styles: Iterable ): Record { + // Use StyleMap to reduce style value to the minimal form (without shorthand and long-hand notation and duplication). + const stylesMap = new StylesMap( viewElement.document.stylesProcessor ); + + for ( const key of styles ) { + const styleValue = viewElement.getStyle( key ); - for ( const prop of iterable ) { - const value = getValue( prop ); - if ( value !== undefined ) { - attributesObject[ prop ] = getValue( prop ); + if ( styleValue !== undefined ) { + stylesMap.set( key, styleValue ); } } - return attributesObject; + return Object.fromEntries( stylesMap.getStylesEntries() ); } /** @@ -942,8 +948,8 @@ function splitPattern( pattern: MatcherObjectPattern, attributeName: 'attributes const attributeValue = pattern[ attributeName ]; if ( isPlainObject( attributeValue ) ) { - return Object.entries( attributeValue as Record ).map( - ( [ key, value ] ) => ( { + return Object.entries( attributeValue as Record ) + .map( ( [ key, value ] ) => ( { name, [ attributeName ]: { [ key ]: value @@ -952,12 +958,11 @@ function splitPattern( pattern: MatcherObjectPattern, attributeName: 'attributes } if ( Array.isArray( attributeValue ) ) { - return attributeValue.map( - value => ( { + return attributeValue + .map( value => ( { name, [ attributeName ]: [ value ] - } ) - ); + } ) ); } return [ pattern ]; diff --git a/packages/ckeditor5-html-support/tests/datafilter.js b/packages/ckeditor5-html-support/tests/datafilter.js index 4e73be87cd1..60a11ff6e51 100644 --- a/packages/ckeditor5-html-support/tests/datafilter.js +++ b/packages/ckeditor5-html-support/tests/datafilter.js @@ -16,7 +16,7 @@ import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_uti import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model.js'; import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view.js'; import { getModelDataWithAttributes } from './_utils/utils.js'; -import { addBackgroundRules } from '@ckeditor/ckeditor5-engine/src/view/styles/background.js'; +import { addBackgroundRules, addBorderRules, addMarginRules, addPaddingRules } from '@ckeditor/ckeditor5-engine'; import { getLabel } from '@ckeditor/ckeditor5-widget/src/utils.js'; import GeneralHtmlSupport from '../src/generalhtmlsupport.js'; @@ -3925,15 +3925,429 @@ describe( 'DataFilter', () => { }, /data-filter-invalid-definition/, null, definition ); } ); - it( 'should handle expanded styles by matcher', () => { - editor.data.addStyleProcessorRules( addBackgroundRules ); + describe( 'expanded styles (shorthand vs longhand notation)', () => { + it( 'should handle expanded styles by matcher', () => { + editor.data.addStyleProcessorRules( addBackgroundRules ); - dataFilter.allowElement( 'p' ); - dataFilter.allowAttributes( { name: 'p', styles: true } ); + dataFilter.allowElement( 'p' ); + dataFilter.allowAttributes( { name: 'p', styles: true } ); + + editor.setData( '

foobar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + styles: { + 'background-color': 'red' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should handle longhand style for shorthand filter (background vs background-color)', () => { + editor.data.addStyleProcessorRules( addBackgroundRules ); + + dataFilter.allowElement( 'p' ); + dataFilter.allowAttributes( { name: 'p', styles: 'background' } ); + + editor.setData( '

foobar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + styles: { + 'background-color': 'red' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should handle shorthand style for longhand filter (background vs background-color)', () => { + editor.data.addStyleProcessorRules( addBackgroundRules ); + + dataFilter.allowElement( 'p' ); + dataFilter.allowAttributes( { name: 'p', styles: 'background' } ); + + editor.setData( '

foobar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + styles: { + 'background-color': 'red' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should handle partial padding for generic padding filter (single box side)', () => { + editor.data.addStyleProcessorRules( addPaddingRules ); + + dataFilter.allowElement( 'p' ); + dataFilter.allowAttributes( { name: 'p', styles: 'padding' } ); + + editor.setData( '

foobar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + styles: { + 'padding-left': '10px' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should handle partial padding for specific full padding filter (single box side)', () => { + editor.data.addStyleProcessorRules( addPaddingRules ); + + dataFilter.allowElement( 'p' ); + dataFilter.allowAttributes( { name: 'p', styles: 'padding-left' } ); + + editor.setData( '

foobar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + styles: { + 'padding-left': '10px' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should handle partial padding for generic padding filter (multiple sides)', () => { + editor.data.addStyleProcessorRules( addPaddingRules ); + + dataFilter.allowElement( 'p' ); + dataFilter.allowAttributes( { name: 'p', styles: 'padding' } ); + + editor.setData( '

foobar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + styles: { + 'padding-bottom': '20px', + 'padding-left': '10px' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should handle partial padding for generic padding filter (box top side)', () => { + editor.data.addStyleProcessorRules( addPaddingRules ); + + dataFilter.allowElement( 'p' ); + dataFilter.allowAttributes( { name: 'p', styles: 'padding' } ); + + editor.setData( '

foobar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + styles: { + 'padding-top': '10px' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should handle partial border for generic border filter (box bottom side)', () => { + editor.data.addStyleProcessorRules( addBorderRules ); + + dataFilter.allowElement( 'p' ); + dataFilter.allowAttributes( { name: 'p', styles: 'border' } ); + + editor.setData( '

foobar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + styles: { + 'border-bottom': '3px dotted red' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should handle partial border for generic border filter (box bottom side style only)', () => { + editor.data.addStyleProcessorRules( addBorderRules ); + + dataFilter.allowElement( 'p' ); + dataFilter.allowAttributes( { name: 'p', styles: 'border' } ); + + editor.setData( '

foobar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + styles: { + 'border-bottom-style': 'dotted' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should handle partial border for generic border filter (box bottom side style and color)', () => { + editor.data.addStyleProcessorRules( addBorderRules ); - editor.setData( '

foobar

' ); + dataFilter.allowElement( 'p' ); + dataFilter.allowAttributes( { name: 'p', styles: 'border' } ); + + editor.setData( '

foobar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + styles: { + 'border-bottom-style': 'dotted', + 'border-bottom-color': 'red' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should handle partial border for generic border filter (missing border color)', () => { + editor.data.addStyleProcessorRules( addBorderRules ); + + dataFilter.allowElement( 'p' ); + dataFilter.allowAttributes( { name: 'p', styles: 'border-left' } ); + + editor.setData( '

foobar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + styles: { + 'border-left-style': 'solid', + 'border-left-width': '1px' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should handle partial border for partial border filter (box bottom side)', () => { + editor.data.addStyleProcessorRules( addBorderRules ); + + dataFilter.allowElement( 'p' ); + dataFilter.allowAttributes( { name: 'p', styles: 'border-bottom' } ); + + editor.setData( '

foobar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + styles: { + 'border-bottom': '3px dotted red' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should handle partial border for partial border filter (color only)', () => { + editor.data.addStyleProcessorRules( addBorderRules ); + + dataFilter.allowElement( 'p' ); + dataFilter.allowAttributes( { name: 'p', styles: 'border-color' } ); + + editor.setData( '

foobar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + styles: { + 'border-color': 'red' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should handle partial border for generic border filter (mixed)', () => { + editor.data.addStyleProcessorRules( addBorderRules ); + + dataFilter.allowElement( 'p' ); + dataFilter.allowAttributes( { name: 'p', styles: 'border' } ); + + editor.setData( '

foobar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + styles: { + 'border-bottom-width': '3px', + 'border-color': 'red', + 'border-style': 'dotted' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should handle partial border for generic border filter (partly consumed)', () => { + editor.data.addStyleProcessorRules( addBorderRules ); + + dataFilter.allowElement( 'p' ); + dataFilter.allowAttributes( { name: 'p', styles: 'border' } ); + + editor.conversion.for( 'upcast' ).add( dispatcher => { + dispatcher.on( 'element:p', ( evt, data, { consumable } ) => { + consumable.consume( data.viewItem, { styles: [ 'border-left-width' ] } ); + } ); + } ); + + editor.setData( '

foobar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + styles: { + 'border-color': 'red', + 'border-style': 'dotted', + 'border-bottom-width': '3px', + 'border-right-width': '3px', + 'border-top-width': '3px' + } + } + } + } ); + + expect( editor.getData() ).to.equal( + '

foobar

' + ); + } ); + + it( 'should handle partial margin consumed for generic margin filter', () => { + editor.data.addStyleProcessorRules( addMarginRules ); + + dataFilter.allowElement( 'p' ); + dataFilter.allowAttributes( { name: 'p', styles: 'margin' } ); + + editor.model.schema.extend( 'paragraph', { allowAttributes: 'indent' } ); + + editor.conversion.for( 'upcast' ).attributeToAttribute( { + view: { + styles: { 'margin-left': /./ } + }, + model: { + key: 'indent', + value: viewElement => `${ parseInt( viewElement.getStyle( 'margin-left' ) ) * 2 }px` + } + } ); - expect( editor.getData() ).to.equal( '

foobar

' ); + editor.conversion.for( 'downcast' ).attributeToAttribute( { + model: 'indent', + view: value => ( { + key: 'style', + value: { 'margin-left': value } + } ) + } ); + + editor.setData( '

foobar

' ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + styles: { + 'margin-top': '20px', + 'margin-right': '20px', + 'margin-bottom': '20px' + } + } + } + } ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should store only reduced styles in model attribute (without duplicated long and shorthand)', () => { + editor.data.addStyleProcessorRules( addBorderRules ); + + dataFilter.allowElement( 'p' ); + dataFilter.allowAttributes( { name: 'p', styles: [ 'border' ] } ); + + editor.setData( + '

foobar

' + ); + + expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( { + data: 'foobar', + attributes: { + 1: { + styles: { + 'border-bottom': '2px solid red' + } + } + } + } ); + + expect( editor.getData() ).to.equal( + '

foobar

' + ); + } ); } ); describe( 'attribute coupling', () => { diff --git a/packages/ckeditor5-image/docs/features/images-responsive.md b/packages/ckeditor5-image/docs/features/images-responsive.md index e5bca314ac7..d8f2726c82b 100644 --- a/packages/ckeditor5-image/docs/features/images-responsive.md +++ b/packages/ckeditor5-image/docs/features/images-responsive.md @@ -73,7 +73,3 @@ Regardless of the original file format, the responsive versions will be served a For detailed information on how to configure and use CKBox, please refer to the {@link features/ckbox#installation CKBox file manager} installation guide. - -## Contribute - -The source code of the feature is available on GitHub at [https://github.com/ckeditor/ckeditor5/tree/master/packages/ckeditor5-link](https://github.com/ckeditor/ckeditor5/tree/master/packages/ckeditor5-link). diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts index 92a5ecda81d..e353ccb2084 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertui.ts @@ -114,11 +114,11 @@ export default class ImageInsertUI extends Plugin { requiresForm }: { name: string; - observable: Observable & { isEnabled: boolean }; + observable: Observable & { isEnabled: boolean } | ( () => Observable & { isEnabled: boolean } ); buttonViewCreator: ( isOnlyOne: boolean ) => ButtonView; formViewCreator: ( isOnlyOne: boolean ) => FocusableView; requiresForm?: boolean; -} ): void { + } ): void { if ( this._integrations.has( name ) ) { /** * There are two insert-image integrations registered with the same name. @@ -174,7 +174,7 @@ export default class ImageInsertUI extends Plugin { } const dropdownView = this.dropdownView = createDropdown( locale, dropdownButton ); - const observables = integrations.map( ( { observable } ) => observable ); + const observables = integrations.map( ( { observable } ) => typeof observable == 'function' ? observable() : observable ); dropdownView.bind( 'isEnabled' ).toMany( observables, 'isEnabled', ( ...isEnabled ) => ( isEnabled.some( isEnabled => isEnabled ) @@ -250,7 +250,7 @@ export default class ImageInsertUI extends Plugin { } type IntegrationData = { - observable: Observable & { isEnabled: boolean }; + observable: Observable & { isEnabled: boolean } | ( () => Observable & { isEnabled: boolean } ); buttonViewCreator: ( isOnlyOne: boolean ) => ButtonView; formViewCreator: ( isOnlyOne: boolean ) => FocusableView; requiresForm: boolean; diff --git a/packages/ckeditor5-image/src/imageinsert/imageinsertviaurlui.ts b/packages/ckeditor5-image/src/imageinsert/imageinsertviaurlui.ts index 4c6e6ecbd91..41e56f7bb03 100644 --- a/packages/ckeditor5-image/src/imageinsert/imageinsertviaurlui.ts +++ b/packages/ckeditor5-image/src/imageinsert/imageinsertviaurlui.ts @@ -45,11 +45,10 @@ export default class ImageInsertViaUrlUI extends Plugin { */ public afterInit(): void { this._imageInsertUI = this.editor.plugins.get( 'ImageInsertUI' ); - const insertImageCommand: InsertImageCommand = this.editor.commands.get( 'insertImage' )!; this._imageInsertUI.registerIntegration( { name: 'url', - observable: insertImageCommand, + observable: () => this.editor.commands.get( 'insertImage' )!, requiresForm: true, buttonViewCreator: isOnlyOne => this._createInsertUrlButton( isOnlyOne ), formViewCreator: isOnlyOne => this._createInsertUrlView( isOnlyOne ) diff --git a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts index a678d1e7d8e..d35bd3d2bd4 100644 --- a/packages/ckeditor5-image/src/imageupload/imageuploadui.ts +++ b/packages/ckeditor5-image/src/imageupload/imageuploadui.ts @@ -72,11 +72,10 @@ export default class ImageUploadUI extends Plugin { if ( editor.plugins.has( 'ImageInsertUI' ) ) { const imageInsertUI: ImageInsertUI = editor.plugins.get( 'ImageInsertUI' ); - const command: UploadImageCommand = editor.commands.get( 'uploadImage' )!; imageInsertUI.registerIntegration( { name: 'upload', - observable: command, + observable: () => editor.commands.get( 'uploadImage' )!, buttonViewCreator: () => { const uploadImageButton = editor.ui.componentFactory.create( 'uploadImage' ) as FileDialogButtonView; diff --git a/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js b/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js index 78f0bf76472..f61c6040842 100644 --- a/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js +++ b/packages/ckeditor5-image/tests/imageinsert/imageinsertui.js @@ -296,6 +296,22 @@ describe( 'ImageInsertUI', () => { } ); } ); + describe( 'single integration with form view required and observalbe as a function', () => { + beforeEach( async () => { + registerUrlIntegration( true ); + } ); + + it( 'should bind isEnabled state to observable', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + observableUrl.isEnabled = false; + expect( dropdown.isEnabled ).to.be.false; + + observableUrl.isEnabled = true; + expect( dropdown.isEnabled ).to.be.true; + } ); + } ); + describe( 'multiple integrations', () => { beforeEach( async () => { registerUploadIntegration(); @@ -365,12 +381,39 @@ describe( 'ImageInsertUI', () => { } ); } ); - function registerUrlIntegration() { + describe( 'multiple integrations and observalbe as a function', () => { + beforeEach( async () => { + registerUploadIntegration( true ); + registerUrlIntegration( true ); + } ); + + it( 'should bind isEnabled state to observables', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + observableUrl.isEnabled = false; + observableUpload.isEnabled = false; + expect( dropdown.isEnabled ).to.be.false; + + observableUrl.isEnabled = true; + observableUpload.isEnabled = false; + expect( dropdown.isEnabled ).to.be.true; + + observableUrl.isEnabled = false; + observableUpload.isEnabled = true; + expect( dropdown.isEnabled ).to.be.true; + + observableUrl.isEnabled = true; + observableUpload.isEnabled = true; + expect( dropdown.isEnabled ).to.be.true; + } ); + } ); + + function registerUrlIntegration( observableAsFunc ) { observableUrl = new Model( { isEnabled: true } ); insertImageUI.registerIntegration( { name: 'url', - observable: observableUrl, + observable: observableAsFunc ? () => observableUrl : observableUrl, requiresForm: true, buttonViewCreator( isOnlyOne ) { const button = new ButtonView( editor.locale ); @@ -389,12 +432,12 @@ describe( 'ImageInsertUI', () => { } ); } - function registerUploadIntegration() { + function registerUploadIntegration( observableAsFunc ) { observableUpload = new Model( { isEnabled: true } ); insertImageUI.registerIntegration( { name: 'upload', - observable: observableUpload, + observable: observableAsFunc ? () => observableUpload : observableUpload, buttonViewCreator( isOnlyOne ) { const button = new ButtonView( editor.locale ); diff --git a/packages/ckeditor5-indent/src/indentui.ts b/packages/ckeditor5-indent/src/indentui.ts index 079125475d7..016c114c2d6 100644 --- a/packages/ckeditor5-indent/src/indentui.ts +++ b/packages/ckeditor5-indent/src/indentui.ts @@ -7,7 +7,7 @@ * @module indent/indentui */ -import { ButtonView } from 'ckeditor5/src/ui.js'; +import { ButtonView, MenuBarMenuListItemButtonView } from 'ckeditor5/src/ui.js'; import { icons, Plugin } from 'ckeditor5/src/core.js'; /** @@ -42,29 +42,54 @@ export default class IndentUI extends Plugin { } /** - * Defines a UI button. + * Defines UI buttons for both toolbar and menu bar. */ private _defineButton( commandName: 'indent' | 'outdent', label: string, icon: string ): void { const editor = this.editor; - editor.ui.componentFactory.add( commandName, locale => { - const command = editor.commands.get( commandName )!; - const view = new ButtonView( locale ); + editor.ui.componentFactory.add( commandName, () => { + const buttonView = this._createButton( ButtonView, commandName, label, icon ); - view.set( { - label, - icon, + buttonView.set( { tooltip: true } ); - view.bind( 'isEnabled' ).to( command, 'isEnabled' ); + return buttonView; + } ); - this.listenTo( view, 'execute', () => { - editor.execute( commandName ); - editor.editing.view.focus(); - } ); + editor.ui.componentFactory.add( 'menuBar:' + commandName, () => { + return this._createButton( MenuBarMenuListItemButtonView, commandName, label, icon ); + } ); + } - return view; + /** + * Creates a button to use either in toolbar or in menu bar. + */ + private _createButton( + ButtonClass: T, + commandName: string, + label: string, + icon: string + ): InstanceType { + const editor = this.editor; + const locale = editor.locale; + const command = editor.commands.get( commandName )!; + const view = new ButtonClass( editor.locale ) as InstanceType; + const t = locale.t; + + view.set( { + label, + icon + } ); + + view.bind( 'isEnabled' ).to( command, 'isEnabled' ); + + // Execute the command. + this.listenTo( view, 'execute', () => { + editor.execute( commandName ); + editor.editing.view.focus(); } ); + + return view; } } diff --git a/packages/ckeditor5-link/lang/contexts.json b/packages/ckeditor5-link/lang/contexts.json index 0d1422d412e..6634c6d1a06 100644 --- a/packages/ckeditor5-link/lang/contexts.json +++ b/packages/ckeditor5-link/lang/contexts.json @@ -7,5 +7,7 @@ "Open link in new tab": "Button opening the link in new browser tab.", "This link has no URL": "Label explaining that a link has no URL set (the URL is empty).", "Open in a new tab": "The label of the switch button that controls whether the edited link will open in a new tab.", - "Downloadable": "The label of the switch button that controls whether the edited link refers to downloadable resource." + "Downloadable": "The label of the switch button that controls whether the edited link refers to downloadable resource.", + "Create link": "Keystroke description for assistive technologies: keystroke for creating a link.", + "Move out of a link": "Keystroke description for assistive technologies: keystroke for moving out of a link." } diff --git a/packages/ckeditor5-link/src/linkui.ts b/packages/ckeditor5-link/src/linkui.ts index 1c20161d38a..acef29027d2 100644 --- a/packages/ckeditor5-link/src/linkui.ts +++ b/packages/ckeditor5-link/src/linkui.ts @@ -76,6 +76,7 @@ export default class LinkUI extends Plugin { */ public init(): void { const editor = this.editor; + const t = this.editor.t; editor.editing.view.addObserver( ClickObserver ); @@ -101,6 +102,23 @@ export default class LinkUI extends Plugin { classes: [ 'ck-fake-link-selection', 'ck-fake-link-selection_collapsed' ] } } ); + + // Add the information about the keystrokes to the accessibility database. + editor.accessibility.addKeystrokeInfos( { + keystrokes: [ + { + label: t( 'Create link' ), + keystroke: LINK_KEYSTROKE + }, + { + label: t( 'Move out of a link' ), + keystroke: [ + [ 'arrowleft', 'arrowleft' ], + [ 'arrowright', 'arrowright' ] + ] + } + ] + } ); } /** diff --git a/packages/ckeditor5-link/tests/linkui.js b/packages/ckeditor5-link/tests/linkui.js index f1469bee14c..d2e49b64493 100644 --- a/packages/ckeditor5-link/tests/linkui.js +++ b/packages/ckeditor5-link/tests/linkui.js @@ -67,6 +67,21 @@ describe( 'LinkUI', () => { expect( editor.plugins.get( ContextualBalloon ) ).to.be.instanceOf( ContextualBalloon ); } ); + it( 'should add keystroke accessibility info', () => { + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'common' ).keystrokes ).to.deep.include( { + label: 'Create link', + keystroke: 'Ctrl+K' + } ); + + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'common' ).keystrokes ).to.deep.include( { + label: 'Move out of a link', + keystroke: [ + [ 'arrowleft', 'arrowleft' ], + [ 'arrowright', 'arrowright' ] + ] + } ); + } ); + describe( 'init', () => { it( 'should register click observer', () => { expect( editor.editing.view.getObserver( ClickObserver ) ).to.be.instanceOf( ClickObserver ); diff --git a/packages/ckeditor5-list/lang/contexts.json b/packages/ckeditor5-list/lang/contexts.json index 2281d7c83b4..c3c21c8f54d 100644 --- a/packages/ckeditor5-list/lang/contexts.json +++ b/packages/ckeditor5-list/lang/contexts.json @@ -24,6 +24,10 @@ "Upper-latin": "The tooltip text of the button that toggles the \"upper–latin\" list style.", "List properties": "The label of the button that toggles the visibility of additional numbered list property UI fields.", "Start at": "The label of the input allowing to change the start index of a numbered list.", + "Invalid start index value.": "The error message displayed when the numbered list start index input value is not a valid number.", "Start index must be greater than 0.": "The error message displayed when the numbered list start index input value is invalid.", - "Reversed order": "The label of the switch button that reverses the order of the numbered list." + "Reversed order": "The label of the switch button that reverses the order of the numbered list.", + "Keystrokes that can be used in a list": "Accessibility help dialog header text displayed before the list of keystrokes that can be used in a list.", + "Increase list item indent": "Keystroke description for assistive technologies: keystroke for increasing list item indentation.", + "Decrease list item indent": "Keystroke description for assistive technologies: keystroke for decreasing list item indentation." } diff --git a/packages/ckeditor5-list/src/list/listediting.ts b/packages/ckeditor5-list/src/list/listediting.ts index eb79e6c033d..8edde75fce1 100644 --- a/packages/ckeditor5-list/src/list/listediting.ts +++ b/packages/ckeditor5-list/src/list/listediting.ts @@ -177,6 +177,7 @@ export default class ListEditing extends Plugin { this._setupEnterIntegration(); this._setupTabIntegration(); this._setupClipboardIntegration(); + this._setupAccessibilityIntegration(); } /** @@ -598,6 +599,29 @@ export default class ListEditing extends Plugin { } ); } ); } + + /** + * Informs editor accessibility features about keystrokes brought by the plugin. + */ + private _setupAccessibilityIntegration() { + const editor = this.editor; + const t = editor.t; + + editor.accessibility.addKeystrokeInfoGroup( { + id: 'list', + label: t( 'Keystrokes that can be used in a list' ), + keystrokes: [ + { + label: t( 'Increase list item indent' ), + keystroke: 'Tab' + }, + { + label: t( 'Decrease list item indent' ), + keystroke: 'Shift+Tab' + } + ] + } ); + } } /** diff --git a/packages/ckeditor5-list/src/listproperties/ui/listpropertiesview.ts b/packages/ckeditor5-list/src/listproperties/ui/listpropertiesview.ts index 0e4d517a86b..50a7566d5eb 100644 --- a/packages/ckeditor5-list/src/listproperties/ui/listpropertiesview.ts +++ b/packages/ckeditor5-list/src/listproperties/ui/listpropertiesview.ts @@ -366,6 +366,10 @@ export default class ListPropertiesView extends View { const startIndex = inputElement.valueAsNumber; if ( Number.isNaN( startIndex ) ) { + // Number inputs allow for the entry of characters that may result in NaN, + // such as 'e', '+', '123e', '2-'. + startIndexFieldView.errorText = t( 'Invalid start index value.' ); + return; } diff --git a/packages/ckeditor5-list/tests/list/listediting.js b/packages/ckeditor5-list/tests/list/listediting.js index ae279e281d4..2c6e538a643 100644 --- a/packages/ckeditor5-list/tests/list/listediting.js +++ b/packages/ckeditor5-list/tests/list/listediting.js @@ -72,6 +72,22 @@ describe( 'ListEditing', () => { expect( ListEditing.pluginName ).to.equal( 'ListEditing' ); } ); + it( 'should add keystroke accessibility info', () => { + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'list' ).label ).to.equal( + 'Keystrokes that can be used in a list' + ); + + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'list' ).keystrokes ).to.deep.include( { + label: 'Increase list item indent', + keystroke: 'Tab' + } ); + + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'list' ).keystrokes ).to.deep.include( { + label: 'Decrease list item indent', + keystroke: 'Shift+Tab' + } ); + } ); + it( 'should be loaded', () => { expect( editor.plugins.get( ListEditing ) ).to.be.instanceOf( ListEditing ); } ); diff --git a/packages/ckeditor5-list/tests/listproperties/ui/listpropertiesview.js b/packages/ckeditor5-list/tests/listproperties/ui/listpropertiesview.js index 0342b31a9ce..62dff282a58 100644 --- a/packages/ckeditor5-list/tests/listproperties/ui/listpropertiesview.js +++ b/packages/ckeditor5-list/tests/listproperties/ui/listpropertiesview.js @@ -698,7 +698,7 @@ describe( 'ListPropertiesView', () => { sinon.assert.notCalled( spy ); } ); - it( 'should not fire #listStart upon #input but display an errir if the field is invalid', () => { + it( 'should not fire #listStart upon #input but display an error if the field is invalid', () => { const spy = sinon.spy(); view.on( 'listStart', spy ); @@ -708,6 +708,36 @@ describe( 'ListPropertiesView', () => { sinon.assert.notCalled( spy ); expect( view.startIndexFieldView.errorText ).to.equal( 'Start index must be greater than 0.' ); } ); + + it( 'should not fire #listStart upon #input but display an error if the numeric value is NaN', () => { + const spy = sinon.spy(); + view.on( 'listStart', spy ); + + view.startIndexFieldView.fieldView.value = '3e'; + view.startIndexFieldView.fieldView.fire( 'input' ); + + sinon.assert.notCalled( spy ); + expect( view.startIndexFieldView.errorText ).to.equal( 'Invalid start index value.' ); + } ); + + it( 'should hide an error and proceed to fire #listStart when previously invalid value gets corrected', () => { + const spy = sinon.spy(); + view.on( 'listStart', spy ); + + // Check for error. + view.startIndexFieldView.fieldView.value = '3e'; + view.startIndexFieldView.fieldView.fire( 'input' ); + + sinon.assert.notCalled( spy ); + expect( view.startIndexFieldView.errorText ).to.equal( 'Invalid start index value.' ); + + // And revert to valid state (clear error). + view.startIndexFieldView.fieldView.value = '32'; + view.startIndexFieldView.fieldView.fire( 'input' ); + + sinon.assert.calledOnce( spy ); + expect( view.startIndexFieldView.errorText ).to.be.null; + } ); } ); describe( '#reversedSwitchButtonView', () => { diff --git a/packages/ckeditor5-minimap/src/minimap.ts b/packages/ckeditor5-minimap/src/minimap.ts index 52afeb2c6e5..a4eb3d342a4 100644 --- a/packages/ckeditor5-minimap/src/minimap.ts +++ b/packages/ckeditor5-minimap/src/minimap.ts @@ -65,6 +65,8 @@ export default class Minimap extends Plugin { * @inheritDoc */ public override destroy(): void { + super.destroy(); + this._minimapView!.destroy(); this._minimapView!.element!.remove(); } @@ -91,6 +93,10 @@ export default class Minimap extends Plugin { this._initializeMinimapView(); this.listenTo( editor.editing.view, 'render', () => { + if ( editor.state !== 'ready' ) { + return; + } + this._syncMinimapToEditingRootScrollPosition(); } ); diff --git a/packages/ckeditor5-page-break/src/pagebreakui.ts b/packages/ckeditor5-page-break/src/pagebreakui.ts index 4ad34d497d8..495070ae828 100644 --- a/packages/ckeditor5-page-break/src/pagebreakui.ts +++ b/packages/ckeditor5-page-break/src/pagebreakui.ts @@ -8,7 +8,7 @@ */ import { Plugin } from 'ckeditor5/src/core.js'; -import { ButtonView } from 'ckeditor5/src/ui.js'; +import { ButtonView, MenuBarMenuListItemButtonView } from 'ckeditor5/src/ui.js'; import pageBreakIcon from '../theme/icons/pagebreak.svg'; @@ -28,28 +28,44 @@ export default class PageBreakUI extends Plugin { */ public init(): void { const editor = this.editor; - const t = editor.t; // Add pageBreak button to feature components. - editor.ui.componentFactory.add( 'pageBreak', locale => { - const command = editor.commands.get( 'pageBreak' )!; - const view = new ButtonView( locale ); + editor.ui.componentFactory.add( 'pageBreak', () => { + const view = this._createButton( ButtonView ); view.set( { - label: t( 'Page break' ), - icon: pageBreakIcon, tooltip: true } ); - view.bind( 'isEnabled' ).to( command, 'isEnabled' ); + return view; + } ); - // Execute command. - this.listenTo( view, 'execute', () => { - editor.execute( 'pageBreak' ); - editor.editing.view.focus(); - } ); + editor.ui.componentFactory.add( 'menuBar:pageBreak', () => this._createButton( MenuBarMenuListItemButtonView ) ); + } - return view; + /** + * Creates a button for page break command to use either in toolbar or in menu bar. + */ + private _createButton( ButtonClass: T ): InstanceType { + const editor = this.editor; + const locale = editor.locale; + const command = editor.commands.get( 'pageBreak' )!; + const view = new ButtonClass( editor.locale ) as InstanceType; + const t = locale.t; + + view.set( { + label: t( 'Page break' ), + icon: pageBreakIcon + } ); + + view.bind( 'isEnabled' ).to( command, 'isEnabled' ); + + // Execute the command. + this.listenTo( view, 'execute', () => { + editor.execute( 'pageBreak' ); + editor.editing.view.focus(); } ); + + return view; } } diff --git a/packages/ckeditor5-remove-format/src/removeformatui.ts b/packages/ckeditor5-remove-format/src/removeformatui.ts index 4b39448443b..0a08d79c9d9 100644 --- a/packages/ckeditor5-remove-format/src/removeformatui.ts +++ b/packages/ckeditor5-remove-format/src/removeformatui.ts @@ -8,7 +8,7 @@ */ import { Plugin } from 'ckeditor5/src/core.js'; -import { ButtonView } from 'ckeditor5/src/ui.js'; +import { ButtonView, MenuBarMenuListItemButtonView } from 'ckeditor5/src/ui.js'; import type RemoveFormatCommand from './removeformatcommand.js'; @@ -33,27 +33,43 @@ export default class RemoveFormatUI extends Plugin { */ public init(): void { const editor = this.editor; - const t = editor.t; - editor.ui.componentFactory.add( REMOVE_FORMAT, locale => { - const command: RemoveFormatCommand = editor.commands.get( REMOVE_FORMAT )!; - const view = new ButtonView( locale ); + editor.ui.componentFactory.add( REMOVE_FORMAT, () => { + const view = this._createButton( ButtonView ); view.set( { - label: t( 'Remove Format' ), - icon: removeFormatIcon, tooltip: true } ); - view.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' ); + return view; + } ); - // Execute the command. - this.listenTo( view, 'execute', () => { - editor.execute( REMOVE_FORMAT ); - editor.editing.view.focus(); - } ); + editor.ui.componentFactory.add( `menuBar:${ REMOVE_FORMAT }`, () => this._createButton( MenuBarMenuListItemButtonView ) ); + } - return view; + /** + * Creates a button for remove format command to use either in toolbar or in menu bar. + */ + private _createButton( ButtonClass: T ): InstanceType { + const editor = this.editor; + const locale = editor.locale; + const command: RemoveFormatCommand = editor.commands.get( REMOVE_FORMAT )!; + const view = new ButtonClass( editor.locale ) as InstanceType; + const t = locale.t; + + view.set( { + label: t( 'Remove Format' ), + icon: removeFormatIcon + } ); + + view.bind( 'isEnabled' ).to( command, 'isEnabled' ); + + // Execute the command. + this.listenTo( view, 'execute', () => { + editor.execute( REMOVE_FORMAT ); + editor.editing.view.focus(); } ); + + return view; } } diff --git a/packages/ckeditor5-select-all/src/selectallediting.ts b/packages/ckeditor5-select-all/src/selectallediting.ts index 024e27d47f9..57d9e76f41e 100644 --- a/packages/ckeditor5-select-all/src/selectallediting.ts +++ b/packages/ckeditor5-select-all/src/selectallediting.ts @@ -33,6 +33,7 @@ export default class SelectAllEditing extends Plugin { */ public init(): void { const editor = this.editor; + const t = editor.t; const view = editor.editing.view; const viewDocument = view.document; @@ -44,5 +45,15 @@ export default class SelectAllEditing extends Plugin { domEventData.preventDefault(); } } ); + + // Add the information about the keystroke to the accessibility database. + editor.accessibility.addKeystrokeInfos( { + keystrokes: [ + { + label: t( 'Select all' ), + keystroke: 'CTRL+A' + } + ] + } ); } } diff --git a/packages/ckeditor5-select-all/tests/selectallediting.js b/packages/ckeditor5-select-all/tests/selectallediting.js index cc40a9ec997..3b02fbd0710 100644 --- a/packages/ckeditor5-select-all/tests/selectallediting.js +++ b/packages/ckeditor5-select-all/tests/selectallediting.js @@ -33,6 +33,13 @@ describe( 'SelectAllEditing', () => { expect( SelectAllEditing.pluginName ).to.equal( 'SelectAllEditing' ); } ); + it( 'should add keystroke accessibility info', () => { + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'common' ).keystrokes ).to.deep.include( { + label: 'Select all', + keystroke: 'CTRL+A' + } ); + } ); + it( 'should register the "selectAll" command', () => { const command = editor.commands.get( 'selectAll' ); diff --git a/packages/ckeditor5-table/lang/contexts.json b/packages/ckeditor5-table/lang/contexts.json index b9b04025c0f..5eac201663d 100644 --- a/packages/ckeditor5-table/lang/contexts.json +++ b/packages/ckeditor5-table/lang/contexts.json @@ -57,7 +57,10 @@ "The color is invalid. Try \"#FF0000\" or \"rgb(255,0,0)\" or \"red\".": "The localized error string that can be displayed next to color (background, border) fields that have an invalid value", "The value is invalid. Try \"10px\" or \"2em\" or simply \"2\".": "The localized error string that can be displayed next to length (padding, border width) fields that have an invalid value.", "Color picker": "The label used by assistive technologies describing a button that opens a color picker, where user can choose a configured color for a certain properties (eg.: background color, color, border-color etc.).", - "Toggle caption off": "The button label for the table toolbar hiding caption attached to the table.", - "Toggle caption on": "The button label for the table toolbar showing caption attached to the table.", - "Enter table caption": "The placeholder text for the table caption displayed when the caption is empty." + "Enter table caption": "The placeholder text for the table caption displayed when the caption is empty.", + "Keystrokes that can be used in a table cell": "Accessibility help dialog header text displayed before the list of keystrokes that can be used in a table cell.", + "Move the selection to the next cell": "Keystroke description for assistive technologies: keystroke for moving the selection to the next cell.", + "Move the selection to the previous cell": "Keystroke description for assistive technologies: keystroke for moving the selection to the previous cell.", + "Insert a new table row (when in the last cell of a table)": "Keystroke description for assistive technologies: keystroke for inserting a new table row.", + "Navigate through the table": "Keystroke description for assistive technologies: keystroke for navigating through the table." } diff --git a/packages/ckeditor5-table/src/tablekeyboard.ts b/packages/ckeditor5-table/src/tablekeyboard.ts index e762a3ec0e7..92d935bdf9b 100644 --- a/packages/ckeditor5-table/src/tablekeyboard.ts +++ b/packages/ckeditor5-table/src/tablekeyboard.ts @@ -52,8 +52,10 @@ export default class TableKeyboard extends Plugin { * @inheritDoc */ public init(): void { - const view = this.editor.editing.view; + const editor = this.editor; + const view = editor.editing.view; const viewDocument = view.document; + const t = editor.t; this.listenTo( viewDocument, @@ -75,6 +77,30 @@ export default class TableKeyboard extends Plugin { ( ...args ) => this._handleTab( ...args ), { context: [ 'th', 'td' ] } ); + + // Add the information about the keystrokes to the accessibility database. + editor.accessibility.addKeystrokeInfoGroup( { + id: 'table', + label: t( 'Keystrokes that can be used in a table cell' ), + keystrokes: [ + { + label: t( 'Move the selection to the next cell' ), + keystroke: 'Tab' + }, + { + label: t( 'Move the selection to the previous cell' ), + keystroke: 'Shift+Tab' + }, + { + label: t( 'Insert a new table row (when in the last cell of a table)' ), + keystroke: 'Tab' + }, + { + label: t( 'Navigate through the table' ), + keystroke: [ [ 'arrowup' ], [ 'arrowright' ], [ 'arrowdown' ], [ 'arrowleft' ] ] + } + ] + } ); } /** diff --git a/packages/ckeditor5-table/tests/tablekeyboard.js b/packages/ckeditor5-table/tests/tablekeyboard.js index a7189172107..65ed7ee96fe 100644 --- a/packages/ckeditor5-table/tests/tablekeyboard.js +++ b/packages/ckeditor5-table/tests/tablekeyboard.js @@ -54,6 +54,32 @@ describe( 'TableKeyboard', () => { expect( TableKeyboard.pluginName ).to.equal( 'TableKeyboard' ); } ); + it( 'should add keystroke accessibility info', () => { + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'table' ).label ).to.equal( + 'Keystrokes that can be used in a table cell' + ); + + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'table' ).keystrokes ).to.deep.include( { + label: 'Move the selection to the next cell', + keystroke: 'Tab' + } ); + + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'table' ).keystrokes ).to.deep.include( { + label: 'Move the selection to the previous cell', + keystroke: 'Shift+Tab' + } ); + + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'table' ).keystrokes ).to.deep.include( { + label: 'Insert a new table row (when in the last cell of a table)', + keystroke: 'Tab' + } ); + + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'table' ).keystrokes ).to.deep.include( { + label: 'Navigate through the table', + keystroke: [ [ 'arrowup' ], [ 'arrowright' ], [ 'arrowdown' ], [ 'arrowleft' ] ] + } ); + } ); + describe( 'Tab key handling', () => { let domEvtDataStub; diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/editorui/accessibilityhelp.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/editorui/accessibilityhelp.css new file mode 100644 index 00000000000..70865719148 --- /dev/null +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/editorui/accessibilityhelp.css @@ -0,0 +1,103 @@ +/* + * 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 "../../../mixins/_focus.css"; +@import "../../../mixins/_shadow.css"; + +:root { + --ck-accessibility-help-dialog-max-width: 600px; + --ck-accessibility-help-dialog-max-height: 400px; + --ck-accessibility-help-dialog-border-color: hsl(220, 6%, 81%); + --ck-accessibility-help-dialog-code-background-color: hsl(0deg 0% 92.94%); + --ck-accessibility-help-dialog-kbd-shadow-color: hsl(0deg 0% 61%); +} + +.ck.ck-accessibility-help-dialog .ck-accessibility-help-dialog__content { + padding: var(--ck-spacing-large); + max-width: var(--ck-accessibility-help-dialog-max-width); + max-height: var(--ck-accessibility-help-dialog-max-height); + overflow: auto; + user-select: text; + + border: 1px solid transparent; + + &:focus { + @mixin ck-focus-ring; + @mixin ck-box-shadow var(--ck-focus-outer-shadow); + } + + * { + white-space: normal; + } + + /* Hide the main label of the content container. */ + & .ck-label { + display: none; + } + + & h3 { + font-weight: bold; + font-size: 1.2em; + } + + & h4 { + font-weight: bold; + font-size: 1em; + } + + & p, + & h3, + & h4, + & table { + margin: 1em 0; + } + + & dl { + display: grid; + grid-template-columns: 2fr 1fr; + border-top: 1px solid var(--ck-accessibility-help-dialog-border-color); + border-bottom: none; + + & dt, & dd { + border-bottom: 1px solid var(--ck-accessibility-help-dialog-border-color); + padding: .4em 0; + } + + & dt { + grid-column-start: 1; + } + + & dd { + grid-column-start: 2; + text-align: right; + } + } + + & kbd, & code { + display: inline-block; + background: var(--ck-accessibility-help-dialog-code-background-color); + padding: .4em; + vertical-align: middle; + line-height: 1; + border-radius: 2px; + text-align: center; + font-size: .9em; + } + + & code { + font-family: monospace; + } + + & kbd { + min-width: 1.8em; + box-shadow: 0px 1px 1px var(--ck-accessibility-help-dialog-kbd-shadow-color); + margin: 0 1px; + + & + kbd { + margin-left: 2px; + } + } +} + diff --git a/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/labeledfield/labeledfieldview.css b/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/labeledfield/labeledfieldview.css index 41ec79dca19..08002b20008 100644 --- a/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/labeledfield/labeledfieldview.css +++ b/packages/ckeditor5-theme-lark/theme/ckeditor5-ui/components/labeledfield/labeledfieldview.css @@ -88,7 +88,7 @@ /* Fields that are disabled or not focused and without a placeholder should have full-sized labels. */ /* stylelint-disable-next-line no-descending-specificity */ &.ck-disabled.ck-labeled-field-view_empty:not(.ck-labeled-field-view_placeholder) > .ck.ck-labeled-field-view__input-wrapper > .ck.ck-label, - &.ck-labeled-field-view_empty:not(.ck-labeled-field-view_focused):not(.ck-labeled-field-view_placeholder) > .ck.ck-labeled-field-view__input-wrapper > .ck.ck-label { + &.ck-labeled-field-view_empty:not(.ck-labeled-field-view_focused):not(.ck-labeled-field-view_placeholder):not(.ck-error) > .ck.ck-labeled-field-view__input-wrapper > .ck.ck-label { @mixin ck-dir ltr { transform: translate(var(--ck-labeled-field-label-default-position-x), var(--ck-labeled-field-label-default-position-y)) scale(1); } diff --git a/packages/ckeditor5-ui/ckeditor5-metadata.json b/packages/ckeditor5-ui/ckeditor5-metadata.json index f68952bd8a3..27ca6f9dca3 100644 --- a/packages/ckeditor5-ui/ckeditor5-metadata.json +++ b/packages/ckeditor5-ui/ckeditor5-metadata.json @@ -6,6 +6,20 @@ "description": "Provides an additional configurable toolbar on the left-hand side of the content area, next to the selected block element. It comes in handy when the main editor toolbar cannot be accessed.", "docs": "features/toolbar/blocktoolbar.html", "path": "src/toolbar/block/blocktoolbar.js" + }, + { + "name": "Accessibility help", + "className": "AccessibilityHelp", + "description": "Displays all editor keyboard shortcuts in a dialog window.", + "docs": "features/keyboard-support.html", + "path": "src/editorui/accessibilityhelp/accessibilityhelp.js", + "uiComponents": [ + { + "type": "Button", + "name": "accessibilityHelp", + "iconPath": "theme/icons/accessibility.svg" + } + ] } ] } diff --git a/packages/ckeditor5-ui/lang/contexts.json b/packages/ckeditor5-ui/lang/contexts.json index cc167270c05..267d46ae22a 100644 --- a/packages/ckeditor5-ui/lang/contexts.json +++ b/packages/ckeditor5-ui/lang/contexts.json @@ -30,5 +30,11 @@ "No results found": "The main text of the message shown to the user when given query does not match any results.", "No searchable items": "The main text of the message shown to the user when no results are available.", "Editor dialog": "A default label of a dialog window displayed on top the editor.", - "Close": "The label and the tooltip for the close button in the dialog header." + "Close": "The label and the tooltip for the close button in the dialog header.", + "Help Contents. To close this dialog press ESC.": "Accessibility help dialog assistive technologies label telling users how to exit the dialog.", + "Below, you can find a list of keyboard shortcuts that can be used in the editor.": "Accessibility help dialog text explaining what can be found in that dialog.", + "(may require Fn)": "Accessibility help dialog text displayed next to keystrokes that may require the Fn key on Mac.", + "Accessibility help": "Accessibility help dialog title.", + "Press %0 for help.": "Assistive technologies label added to each editor editing area informing users about the possibility of opening the accessibility help dialog.", + "Move focus in and out of an active dialog window": "Keystroke description for assistive technologies: keystroke for moving focus out of an active dialog window." } diff --git a/packages/ckeditor5-ui/package.json b/packages/ckeditor5-ui/package.json index a022aaecdcc..554ed44dbf7 100644 --- a/packages/ckeditor5-ui/package.json +++ b/packages/ckeditor5-ui/package.json @@ -47,6 +47,7 @@ "@ckeditor/ckeditor5-special-characters": "41.1.0", "@ckeditor/ckeditor5-table": "41.1.0", "@ckeditor/ckeditor5-typing": "41.1.0", + "@ckeditor/ckeditor5-undo": "41.1.0", "@types/color-convert": "2.0.0", "typescript": "5.0.4", "webpack": "^5.58.1", diff --git a/packages/ckeditor5-ui/src/augmentation.ts b/packages/ckeditor5-ui/src/augmentation.ts index e26c5f84378..fc414d295b0 100644 --- a/packages/ckeditor5-ui/src/augmentation.ts +++ b/packages/ckeditor5-ui/src/augmentation.ts @@ -8,7 +8,8 @@ import type { BlockToolbar, ContextualBalloon, Notification, - Dialog + Dialog, + AccessibilityHelp } from './index.js'; import type { @@ -97,5 +98,6 @@ declare module '@ckeditor/ckeditor5-core' { [ ContextualBalloon.pluginName ]: ContextualBalloon; [ Dialog.pluginName ]: Dialog; [ Notification.pluginName ]: Notification; + [ AccessibilityHelp.pluginName ]: AccessibilityHelp; } } diff --git a/packages/ckeditor5-ui/src/dialog/dialog.ts b/packages/ckeditor5-ui/src/dialog/dialog.ts index 6ac2ec81ef6..eba7c685acc 100644 --- a/packages/ckeditor5-ui/src/dialog/dialog.ts +++ b/packages/ckeditor5-ui/src/dialog/dialog.ts @@ -65,11 +65,23 @@ export default class Dialog extends Plugin { constructor( editor: Editor ) { super( editor ); + const t = editor.t; + this._initShowHideListeners(); this._initFocusToggler(); this._initMultiRootIntegration(); this.set( 'id', null ); + + // Add the information about the keystroke to the accessibility database. + editor.accessibility.addKeystrokeInfos( { + categoryId: 'navigation', + keystrokes: [ { + label: t( 'Move focus in and out of an active dialog window' ), + keystroke: 'Ctrl+F6', + mayRequireFn: true + } ] + } ); } /** diff --git a/packages/ckeditor5-ui/src/editorui/accessibilityhelp/accessibilityhelp.ts b/packages/ckeditor5-ui/src/editorui/accessibilityhelp/accessibilityhelp.ts new file mode 100644 index 00000000000..27b050a3ca1 --- /dev/null +++ b/packages/ckeditor5-ui/src/editorui/accessibilityhelp/accessibilityhelp.ts @@ -0,0 +1,132 @@ +/** + * @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/accessibilityhelp/accessibilityhelp + */ + +import { Plugin } from '@ckeditor/ckeditor5-core'; +import { ButtonView, Dialog, type EditorUIReadyEvent } from '../../index.js'; +import AccessibilityHelpContentView from './accessibilityhelpcontentview.js'; +import { getEnvKeystrokeText } from '@ckeditor/ckeditor5-utils'; +import type { AddRootEvent } from '@ckeditor/ckeditor5-editor-multi-root'; +import type { DowncastWriter, ViewRootEditableElement } from '@ckeditor/ckeditor5-engine'; + +import accessibilityIcon from '../../../theme/icons/accessibility.svg'; +import '../../../theme/components/editorui/accessibilityhelp.css'; + +/** + * A plugin that brings the accessibility help dialog to the editor available under the Alt+0 + * keystroke and via the "Accessibility help" toolbar button. The dialog displays a list of keystrokes that can be used + * by the user to perform various actions in the editor. + * + * Keystroke information is loaded from {@link module:core/accessibility~Accessibility#keystrokeInfos}. New entries can be + * added using the API provided by the {@link module:core/accessibility~Accessibility} class. + */ +export default class AccessibilityHelp extends Plugin { + /** + * The view that displays the dialog content (list of keystrokes). + * Created when the dialog is opened for the first time. + */ + public contentView: AccessibilityHelpContentView | null = null; + + /** + * @inheritDoc + */ + public static get requires() { + return [ Dialog ] as const; + } + + /** + * @inheritDoc + */ + public static get pluginName() { + return 'AccessibilityHelp' as const; + } + + /** + * @inheritDoc + */ + public init(): void { + const editor = this.editor; + const t = editor.locale.t; + + editor.ui.componentFactory.add( 'accessibilityHelp', locale => { + const buttonView = new ButtonView( locale ); + + buttonView.set( { + label: t( 'Accessibility help' ), + tooltip: true, + withText: false, + keystroke: 'Alt+0', + icon: accessibilityIcon + } ); + + buttonView.on( 'execute', () => this._showDialog() ); + + return buttonView; + } ); + + editor.keystrokes.set( 'Alt+0', ( evt, cancel ) => { + this._showDialog(); + cancel(); + } ); + + this._setupRootLabels(); + } + + /** + * Injects a help text into each editing root's `aria-label` attribute allowing assistive technology users + * to discover the availability of the Accessibility help dialog. + */ + private _setupRootLabels() { + const editor = this.editor; + const editingView = editor.editing.view; + const t = editor.t; + + editor.ui.on( 'ready', () => { + editingView.change( writer => { + for ( const root of editingView.document.roots ) { + addAriaLabelTextToRoot( writer, root ); + } + } ); + + editor.on( 'addRoot', ( evt, modelRoot ) => { + const viewRoot = editor.editing.view.document.getRoot( modelRoot.rootName )!; + + editingView.change( writer => addAriaLabelTextToRoot( writer, viewRoot ) ); + }, { priority: 'low' } ); + } ); + + function addAriaLabelTextToRoot( writer: DowncastWriter, viewRoot: ViewRootEditableElement ) { + const currentAriaLabel = viewRoot.getAttribute( 'aria-label' ); + const newAriaLabel = `${ currentAriaLabel }. ${ t( 'Press %0 for help.', [ getEnvKeystrokeText( 'Alt+0' ) ] ) }`; + + writer.setAttribute( 'aria-label', newAriaLabel, viewRoot ); + } + } + + /** + * Shows the accessibility help dialog. Also, creates {@link #contentView} on demand. + */ + private _showDialog() { + const editor = this.editor; + const dialog = editor.plugins.get( 'Dialog' ); + const t = editor.locale.t; + + if ( !this.contentView ) { + this.contentView = new AccessibilityHelpContentView( editor.locale, editor.accessibility.keystrokeInfos ); + } + + dialog.show( { + id: 'accessibilityHelp', + className: 'ck-accessibility-help-dialog', + title: t( 'Accessibility help' ), + icon: accessibilityIcon, + hasCloseButton: true, + content: this.contentView + } ); + } +} diff --git a/packages/ckeditor5-ui/src/editorui/accessibilityhelp/accessibilityhelpcontentview.ts b/packages/ckeditor5-ui/src/editorui/accessibilityhelp/accessibilityhelpcontentview.ts new file mode 100644 index 00000000000..d809fe186db --- /dev/null +++ b/packages/ckeditor5-ui/src/editorui/accessibilityhelp/accessibilityhelpcontentview.ts @@ -0,0 +1,147 @@ +/** + * @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/accessibilityhelp/accessibilityhelpcontentview + */ + +import { + createElement, + env, + getEnvKeystrokeText, + type Locale +} from '@ckeditor/ckeditor5-utils'; + +import View from '../../view.js'; +import LabelView from '../../label/labelview.js'; +import type { + KeystrokeInfoCategoryDefinition, + KeystrokeInfoDefinition, + KeystrokeInfoDefinitions, + KeystrokeInfoGroupDefinition +} from '@ckeditor/ckeditor5-core'; + +/** + * The view displaying keystrokes in the Accessibility help dialog. + */ +export default class AccessibilityHelpContentView extends View { + /** + * @inheritDoc + */ + constructor( locale: Locale, keystrokes: KeystrokeInfoDefinitions ) { + super( locale ); + + const t = locale.t; + const helpLabel = new LabelView(); + + helpLabel.text = t( 'Help Contents. To close this dialog press ESC.' ); + + this.setTemplate( { + tag: 'div', + attributes: { + class: [ 'ck', 'ck-accessibility-help-dialog__content' ], + 'aria-labelledby': helpLabel.id, + role: 'document', + tabindex: -1 + }, + children: [ + createElement( document, 'p', {}, t( 'Below, you can find a list of keyboard shortcuts that can be used in the editor.' ) ), + ...this._createCategories( Array.from( keystrokes.values() ) ), + helpLabel + ] + } ); + } + + /** + * @inheritDoc + */ + public focus(): void { + this.element!.focus(); + } + + /** + * Creates `

Category label

...
` elements for each category of keystrokes. + */ + private _createCategories( categories: Array ): Array { + return categories.map( categoryDefinition => { + const elements: Array = [ + // Category header. + createElement( document, 'h3', {}, categoryDefinition.label ), + + // Category definitions (
) and their optional headers (

). + ...Array.from( categoryDefinition.groups.values() ) + .map( groupDefinition => this._createGroup( groupDefinition ) ) + .flat() + ]; + + // Category description (

). + if ( categoryDefinition.description ) { + elements.splice( 1, 0, createElement( document, 'p', {}, categoryDefinition.description ) ); + } + + return createElement( document, 'section', {}, elements ); + } ); + } + + /** + * Creates `[

Optional label

]
...
` elements for each group of keystrokes in a category. + */ + private _createGroup( groupDefinition: KeystrokeInfoGroupDefinition ): Array { + const definitionAndDescriptionElements = groupDefinition.keystrokes + .sort( ( a, b ) => a.label.localeCompare( b.label ) ) + .map( keystrokeDefinition => this._createGroupRow( keystrokeDefinition ) ) + .flat(); + + const elements: Array = [ + createElement( document, 'dl', {}, definitionAndDescriptionElements ) + ]; + + if ( groupDefinition.label ) { + elements.unshift( createElement( document, 'h4', {}, groupDefinition.label ) ); + } + + return elements; + } + + /** + * Creates `
Keystroke label
Keystroke definition
` elements for each keystroke in a group. + */ + private _createGroupRow( keystrokeDefinition: KeystrokeInfoDefinition ): [ HTMLElement, HTMLElement ] { + const t = this.locale!.t; + const dt = createElement( document, 'dt' ); + const dd = createElement( document, 'dd' ); + const normalizedKeystrokeDefinition = normalizeKeystrokeDefinition( keystrokeDefinition.keystroke ); + const keystrokeAlternativeHTMLs = []; + + for ( const keystrokeAlternative of normalizedKeystrokeDefinition ) { + keystrokeAlternativeHTMLs.push( keystrokeAlternative.map( keystrokeToEnvKbd ).join( '' ) ); + } + + dt.innerHTML = keystrokeDefinition.label; + dd.innerHTML = keystrokeAlternativeHTMLs.join( ', ' ) + + ( keystrokeDefinition.mayRequireFn && env.isMac ? ` ${ t( '(may require Fn)' ) }` : '' ); + + return [ dt, dd ]; + } +} + +function keystrokeToEnvKbd( keystroke: string ): string { + return getEnvKeystrokeText( keystroke ) + .split( '+' ) + .map( part => `${ part }` ) + .join( '+' ); +} + +function normalizeKeystrokeDefinition( definition: KeystrokeInfoDefinition[ 'keystroke' ] ): Array> { + if ( typeof definition === 'string' ) { + return [ [ definition ] ]; + } + + if ( typeof definition[ 0 ] === 'string' ) { + return [ definition as Array ]; + } + + return definition as Array>; +} diff --git a/packages/ckeditor5-ui/src/formheader/formheaderview.ts b/packages/ckeditor5-ui/src/formheader/formheaderview.ts index d783329de72..6141c570b98 100644 --- a/packages/ckeditor5-ui/src/formheader/formheaderview.ts +++ b/packages/ckeditor5-ui/src/formheader/formheaderview.ts @@ -104,7 +104,8 @@ export default class FormHeaderView extends View { class: [ 'ck', 'ck-form__header__label' - ] + ], + role: 'presentation' }, children: [ { text: bind.to( 'label' ) } diff --git a/packages/ckeditor5-ui/src/index.ts b/packages/ckeditor5-ui/src/index.ts index 26410a88bd9..b2b28890495 100644 --- a/packages/ckeditor5-ui/src/index.ts +++ b/packages/ckeditor5-ui/src/index.ts @@ -13,6 +13,8 @@ export { default as CssTransitionDisablerMixin, type ViewWithCssTransitionDisabl export { default as submitHandler } from './bindings/submithandler.js'; export { default as addKeyboardHandlingForGrid } from './bindings/addkeyboardhandlingforgrid.js'; +export { default as AccessibilityHelp } from './editorui/accessibilityhelp/accessibilityhelp.js'; + export { default as BodyCollection } from './editorui/bodycollection.js'; export { type ButtonExecuteEvent } from './button/button.js'; diff --git a/packages/ckeditor5-ui/src/menubar/menubarview.ts b/packages/ckeditor5-ui/src/menubar/menubarview.ts index 2db5a40bca4..beec22f4202 100644 --- a/packages/ckeditor5-ui/src/menubar/menubarview.ts +++ b/packages/ckeditor5-ui/src/menubar/menubarview.ts @@ -149,6 +149,10 @@ export default class MenuBarView extends View implements FocusableView { id: 'insert', label: 'Insert', items: [ + 'menuBar:blockQuote', + 'menuBar:htmlEmbed', + 'menuBar:pageBreak', + 'menuBar:horizontalLine', 'menuBar:blockQuote' ] }, @@ -166,7 +170,11 @@ export default class MenuBarView extends View implements FocusableView { 'menuBar:numberedList', 'menuBar:todoList', '-', - 'menuBar:heading' + 'menuBar:heading', + '-', + 'menuBar:indent', + 'menuBar:outdent', + 'menuBar:removeFormat' ] }, { diff --git a/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.ts b/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.ts index 301e4f642ab..8a59bc5fd0d 100644 --- a/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.ts +++ b/packages/ckeditor5-ui/src/toolbar/balloon/balloontoolbar.ts @@ -192,16 +192,12 @@ export default class BalloonToolbar extends Plugin { this.listenTo( this.toolbarView, 'groupedItemsUpdate', () => { this._updatePosition(); } ); - } - - /** - * Creates toolbar components based on given configuration. - * This needs to be done when all plugins are ready. - */ - public afterInit(): void { - const factory = this.editor.ui.componentFactory; - this.toolbarView.fillFromConfig( this._balloonConfig, factory ); + // Creates toolbar components based on given configuration. + // This needs to be done when all plugins are ready. + editor.ui.once( 'ready', () => { + this.toolbarView.fillFromConfig( this._balloonConfig, this.editor.ui.componentFactory ); + } ); } /** diff --git a/packages/ckeditor5-ui/src/toolbar/block/blocktoolbar.ts b/packages/ckeditor5-ui/src/toolbar/block/blocktoolbar.ts index 1a79898426c..97d8808b149 100644 --- a/packages/ckeditor5-ui/src/toolbar/block/blocktoolbar.ts +++ b/packages/ckeditor5-ui/src/toolbar/block/blocktoolbar.ts @@ -30,7 +30,7 @@ import clickOutsideHandler from '../../bindings/clickoutsidehandler.js'; import normalizeToolbarConfig from '../normalizetoolbarconfig.js'; import type { ButtonExecuteEvent } from '../../button/button.js'; -import type { EditorUIUpdateEvent } from '../../editorui/editorui.js'; +import type { EditorUIReadyEvent, EditorUIUpdateEvent } from '../../editorui/editorui.js'; const toPx = toUnit( 'px' ); @@ -191,20 +191,17 @@ export default class BlockToolbar extends Plugin { beforeFocus: () => this._showPanel(), afterBlur: () => this._hidePanel() } ); - } - /** - * Fills the toolbar with its items based on the configuration. - * - * **Note:** This needs to be done after all plugins are ready. - */ - public afterInit(): void { - this.toolbarView.fillFromConfig( this._blockToolbarConfig, this.editor.ui.componentFactory ); + // Fills the toolbar with its items based on the configuration. + // This needs to be done after all plugins are ready. + editor.ui.once( 'ready', () => { + this.toolbarView.fillFromConfig( this._blockToolbarConfig, this.editor.ui.componentFactory ); - // Hide panel before executing each button in the panel. - for ( const item of this.toolbarView.items ) { - item.on( 'execute', () => this._hidePanel( true ), { priority: 'high' } ); - } + // Hide panel before executing each button in the panel. + for ( const item of this.toolbarView.items ) { + item.on( 'execute', () => this._hidePanel( true ), { priority: 'high' } ); + } + } ); } /** diff --git a/packages/ckeditor5-ui/tests/dialog/dialog.js b/packages/ckeditor5-ui/tests/dialog/dialog.js index e37cd4b44f9..6c64b9cb1ed 100644 --- a/packages/ckeditor5-ui/tests/dialog/dialog.js +++ b/packages/ckeditor5-ui/tests/dialog/dialog.js @@ -37,6 +37,18 @@ describe( 'Dialog', () => { Dialog._visibleDialogPlugin = undefined; } ); + it( 'should have a name', () => { + expect( Dialog.pluginName ).to.equal( 'Dialog' ); + } ); + + it( 'should add keystroke accessibility info', () => { + expect( editor.accessibility.keystrokeInfos.get( 'navigation' ).groups.get( 'common' ).keystrokes ).to.deep.include( { + label: 'Move focus in and out of an active dialog window', + keystroke: 'Ctrl+F6', + mayRequireFn: true + } ); + } ); + it( 'should initialise without #_visibleDialogPlugin set', () => { expect( Dialog._visibleDialogPlugin ).to.be.undefined; } ); diff --git a/packages/ckeditor5-ui/tests/editorui/accessibilityhelp/accessibilityhelp.js b/packages/ckeditor5-ui/tests/editorui/accessibilityhelp/accessibilityhelp.js new file mode 100644 index 00000000000..c18b13d9996 --- /dev/null +++ b/packages/ckeditor5-ui/tests/editorui/accessibilityhelp/accessibilityhelp.js @@ -0,0 +1,178 @@ +/** + * @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 ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor.js'; +import { AccessibilityHelp, ButtonView } from '../../../src/index.js'; +import { env, global, keyCodes } from '@ckeditor/ckeditor5-utils'; +import { MultiRootEditor } from '@ckeditor/ckeditor5-editor-multi-root'; +import AccessibilityHelpContentView from '../../../src/editorui/accessibilityhelp/accessibilityhelpcontentview.js'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils.js'; +import { UndoEditing } from '@ckeditor/ckeditor5-undo'; + +describe( 'AccessibilityHelp', () => { + let editor, plugin, dialogPlugin, domElement; + + testUtils.createSinonSandbox(); + + beforeEach( async () => { + testUtils.sinon.stub( env, 'isMac' ).value( false ); + domElement = global.document.createElement( 'div' ); + global.document.body.appendChild( domElement ); + + editor = await ClassicTestEditor.create( domElement, { + plugins: [ + AccessibilityHelp + ] + } ); + + plugin = editor.plugins.get( AccessibilityHelp ); + dialogPlugin = editor.plugins.get( 'Dialog' ); + } ); + + afterEach( async () => { + domElement.remove(); + await editor.destroy(); + } ); + + it( 'should have a name', () => { + expect( AccessibilityHelp.pluginName ).to.equal( 'AccessibilityHelp' ); + } ); + + describe( 'constructor()', () => { + it( 'should have #contentView', () => { + expect( plugin.contentView ).to.be.null; + } ); + } ); + + describe( 'init()', () => { + it( 'should register the "accessibilityHelp" button in the factory that opens the dialog', () => { + const buttonView = editor.ui.componentFactory.create( 'accessibilityHelp' ); + const dialogShowSpy = sinon.spy(); + dialogPlugin.on( 'show:accessibilityHelp', dialogShowSpy ); + + expect( buttonView ).to.be.instanceOf( ButtonView ); + expect( buttonView.isOn ).to.be.false; + expect( buttonView.label ).to.equal( 'Accessibility help' ); + expect( buttonView.icon ).to.match( / { + const dialogShowSpy = sinon.spy(); + const keyEventData = { + keyCode: keyCodes[ '0' ], + altKey: true, + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + + dialogPlugin.on( 'show:accessibilityHelp', dialogShowSpy ); + + const wasHandled = editor.keystrokes.press( keyEventData ); + + expect( wasHandled ).to.be.true; + expect( keyEventData.preventDefault.calledOnce ).to.be.true; + + sinon.assert.calledOnce( dialogShowSpy ); + } ); + + describe( 'editor editing view root integration', () => { + it( 'should inject label into a single root', () => { + const viewRoot = editor.editing.view.document.getRoot( 'main' ); + const ariaLabel = viewRoot.getAttribute( 'aria-label' ); + + expect( ariaLabel ).to.equal( 'Editor editing area: main. Press Alt+0 for help.' ); + } ); + + it( 'should work for multiple roots (MultiRootEditor)', async () => { + const rootElA = global.document.createElement( 'div' ); + const rootElB = global.document.createElement( 'div' ); + const rootElC = global.document.createElement( 'div' ); + + global.document.body.appendChild( rootElA ); + global.document.body.appendChild( rootElB ); + global.document.body.appendChild( rootElC ); + + const multiRootEditor = await MultiRootEditor.create( { rootElA, rootElB, rootElC }, { + plugins: [ AccessibilityHelp ] + } ); + + assertEditorRootLabels( multiRootEditor ); + + await multiRootEditor.destroy(); + + for ( const editable of Object.values( multiRootEditor.ui.view.editables ) ) { + editable.element.remove(); + } + } ); + + it( 'should work for dynamic roots', async () => { + const rootElA = global.document.createElement( 'div' ); + const rootElB = global.document.createElement( 'div' ); + const rootElC = global.document.createElement( 'div' ); + + global.document.body.appendChild( rootElA ); + global.document.body.appendChild( rootElB ); + global.document.body.appendChild( rootElC ); + + const multiRootEditor = await MultiRootEditor.create( { rootElA, rootElB, rootElC }, { + plugins: [ AccessibilityHelp, UndoEditing ] + } ); + + multiRootEditor.on( 'addRoot', ( evt, root ) => { + const domElement = multiRootEditor.createEditable( root ); + global.document.body.appendChild( domElement ); + } ); + + multiRootEditor.on( 'detachRoot', ( evt, root ) => { + const domElement = multiRootEditor.detachEditable( root ); + domElement.remove(); + } ); + + multiRootEditor.addRoot( 'dynamicRoot', { isUndoable: true } ); + + assertEditorRootLabels( multiRootEditor ); + + multiRootEditor.detachRoot( multiRootEditor.model.document.getRoot( 'dynamicRoot' ), true ); + + assertEditorRootLabels( multiRootEditor ); + + multiRootEditor.execute( 'undo' ); + + assertEditorRootLabels( multiRootEditor ); + + await multiRootEditor.destroy(); + + for ( const editable of Object.values( multiRootEditor.ui.view.editables ) ) { + editable.element.remove(); + } + } ); + + function assertEditorRootLabels( editor ) { + for ( const rootName of editor.model.document.getRootNames() ) { + const viewRoot = editor.editing.view.document.getRoot( rootName ); + const ariaLabel = viewRoot.getAttribute( 'aria-label' ); + + expect( ariaLabel ).to.equal( `Rich Text Editor. Editing area: ${ rootName }. Press Alt+0 for help.` ); + } + } + } ); + } ); + + describe( 'showing the dialog for the first time', () => { + it( 'should create #contentView', () => { + expect( plugin.contentView ).to.be.null; + + plugin._showDialog(); + + expect( plugin.contentView ).to.be.instanceof( AccessibilityHelpContentView ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-ui/tests/editorui/accessibilityhelp/accessibilityhelpcontentview.js b/packages/ckeditor5-ui/tests/editorui/accessibilityhelp/accessibilityhelpcontentview.js new file mode 100644 index 00000000000..e3470cf3c62 --- /dev/null +++ b/packages/ckeditor5-ui/tests/editorui/accessibilityhelp/accessibilityhelpcontentview.js @@ -0,0 +1,429 @@ +/** + * @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 */ + +import { Locale, env } from '@ckeditor/ckeditor5-utils'; +import AccessibilityHelpContentView from '../../../src/editorui/accessibilityhelp/accessibilityhelpcontentview.js'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils.js'; + +describe( 'AccessibilityHelpContentView', () => { + const defaultKeystrokes = new Map( [ + [ + 'testCat', + { + id: 'testCat', + label: 'Test cat', + groups: new Map( [ + [ + 'testGroup', + { + id: 'testGroup', + label: 'Test group', + keystrokes: [] + } + ] + ] ) + } + ] + ] ); + + testUtils.createSinonSandbox(); + + describe( 'constructor()', () => { + let view; + + beforeEach( () => { + view = getView( defaultKeystrokes ); + view.render(); + } ); + + afterEach( () => { + view.destroy(); + } ); + + it( 'should have label', () => { + expect( view.element.lastChild.classList.contains( 'ck-label' ) ).to.be.true; + expect( view.element.lastChild.id ).to.equal( view.element.getAttribute( 'aria-labelledby' ) ); + } ); + + it( 'should have CSS class', () => { + expect( view.element.classList.contains( 'ck' ) ).to.be.true; + expect( view.element.classList.contains( 'ck-accessibility-help-dialog__content' ) ).to.be.true; + } ); + + it( 'should have the role attribute', () => { + expect( view.element.getAttribute( 'role' ) ).to.equal( 'document' ); + } ); + + it( 'should have tabindex', () => { + expect( view.element.getAttribute( 'tabindex' ) ).to.equal( '-1' ); + } ); + + it( 'should render an intro paragraph', () => { + expect( view.element.firstChild.outerHTML ).to.match( /^

Below, .+<\/p>$/ ); + } ); + } ); + + describe( 'lists of keystrokes', () => { + let view; + + beforeEach( () => { + testUtils.sinon.stub( env, 'isMac' ).value( false ); + } ); + + afterEach( () => { + view.destroy(); + } ); + + it( 'should render for multiple categories, groups, and keystrokes', () => { + view = getView( new Map( [ + [ + 'catA', + { + id: 'catA', + label: 'Cat A', + groups: new Map( [ + [ + 'groupAA', + { + id: 'groupAA', + label: 'Group AA', + keystrokes: [ + { + keystroke: 'Ctrl+A', + label: 'Foo' + }, + { + keystroke: 'Ctrl+B', + label: 'Bar' + } + ] + } + ], + [ + 'groupAB', + { + id: 'groupAB', + label: 'Group AB', + keystrokes: [ + { + keystroke: 'Ctrl+C', + label: 'Baz' + } + ] + } + ] + ] ) + } + ], + [ + 'catB', + { + id: 'catB', + label: 'Cat B', + description: 'Cat B description', + groups: new Map( [ + [ + 'groupBA', + { + id: 'groupBA', + label: 'Group BA', + keystrokes: [ + { + keystroke: 'Ctrl+D', + label: 'Qux' + } + ] + } + ] + ] ) + } + ] + ] ) ); + + view.render(); + + expect( view.element.childNodes[ 1 ].outerHTML ).to.deep.equal( + '

' + + '

Cat A

' + + '

Group AA

' + + '
' + + '
Bar
Ctrl+B
' + + '
Foo
Ctrl+A
' + + '
' + + '

Group AB

' + + '
' + + '
Baz
Ctrl+C
' + + '
' + + '
' + ); + + expect( view.element.childNodes[ 2 ].outerHTML ).to.deep.equal( + '
' + + '

Cat B

' + + '

Cat B description

' + + '

Group BA

' + + '
' + + '
Qux
Ctrl+D
' + + '
' + + '
' + ); + } ); + + it( 'should sort keystrokes alphabetically', () => { + view = getView( new Map( [ + [ + 'catA', + { + id: 'catA', + label: 'Cat A', + groups: new Map( [ + [ + 'groupAA', + { + id: 'groupAA', + label: 'Group AA', + keystrokes: [ + { + keystroke: 'Ctrl+C', + label: 'C' + }, + { + keystroke: 'Ctrl+A', + label: 'A' + }, + { + keystroke: 'Ctrl+B', + label: 'B' + } + ] + } + ] + ] ) + } + ] + + ] ) ); + + view.render(); + + expect( view.element.childNodes[ 1 ].outerHTML ).to.deep.equal( + '
' + + '

Cat A

' + + '

Group AA

' + + '
' + + '
A
Ctrl+A
' + + '
B
Ctrl+B
' + + '
C
Ctrl+C
' + + '
' + + '
' + ); + } ); + + it( 'should use env-specific keystroke rendering', () => { + testUtils.sinon.stub( env, 'isMac' ).value( true ); + + view = getView( new Map( [ + [ + 'catA', + { + id: 'catA', + label: 'Cat A', + groups: new Map( [ + [ + 'groupAA', + { + id: 'groupAA', + label: 'Group AA', + keystrokes: [ + { + keystroke: 'Ctrl+C', + label: 'C' + }, + { + keystroke: 'Alt+A', + label: 'A' + }, + { + keystroke: 'Shift+B', + label: 'B' + } + ] + } + ] + ] ) + } + ] + + ] ) ); + + view.render(); + + expect( view.element.childNodes[ 1 ].outerHTML ).to.deep.equal( + '
' + + '

Cat A

' + + '

Group AA

' + + '
' + + '
A
⌥A
' + + '
B
⇧B
' + + '
C
⌘C
' + + '
' + + '
' + ); + } ); + + it( 'should support the "mayRequireFn" flag in keystroke definition', () => { + testUtils.sinon.stub( env, 'isMac' ).value( true ); + + view = getView( new Map( [ + [ + 'catA', + { + id: 'catA', + label: 'Cat A', + groups: new Map( [ + [ + 'groupAA', + { + id: 'groupAA', + label: 'Group AA', + keystrokes: [ + { + keystroke: 'Alt+A', + label: 'A', + mayRequireFn: true + } + ] + } + ] + ] ) + } + ] + + ] ) ); + + view.render(); + + expect( view.element.childNodes[ 1 ].outerHTML ).to.deep.equal( + '
' + + '

Cat A

' + + '

Group AA

' + + '
' + + '
A
⌥A (may require Fn)
' + + '
' + + '
' + ); + } ); + + it( 'should support keystroke sequences', () => { + view = getView( new Map( [ + [ + 'catA', + { + id: 'catA', + label: 'Cat A', + groups: new Map( [ + [ + 'groupAA', + { + id: 'groupAA', + label: 'Group AA', + keystrokes: [ + { + keystroke: [ 'Alt+A', 'Alt+B' ], + label: 'A' + } + ] + } + ] + ] ) + } + ] + + ] ) ); + + view.render(); + + expect( view.element.childNodes[ 1 ].outerHTML ).to.deep.equal( + '
' + + '

Cat A

' + + '

Group AA

' + + '
' + + '
A
' + + '
' + + 'Alt+A' + + 'Alt+B' + + '
' + + '
' + + '
' + ); + } ); + + it( 'should support keystroke alternatives', () => { + view = getView( new Map( [ + [ + 'catA', + { + id: 'catA', + label: 'Cat A', + groups: new Map( [ + [ + 'groupAA', + { + id: 'groupAA', + label: 'Group AA', + keystrokes: [ + { + keystroke: [ [ 'Alt+A', 'Alt+B' ], [ 'Alt+C', 'Alt+D' ] ], + label: 'A' + } + ] + } + ] + ] ) + } + ] + + ] ) ); + + view.render(); + + expect( view.element.childNodes[ 1 ].outerHTML ).to.deep.equal( + '
' + + '

Cat A

' + + '

Group AA

' + + '
' + + '
A
' + + '
' + + 'Alt+AAlt+B, ' + + 'Alt+CAlt+D' + + '
' + + '
' + + '
' + ); + } ); + } ); + + describe( 'focus()', () => { + it( 'should focus the view', () => { + const view = getView( defaultKeystrokes ); + view.render(); + const focusSpy = sinon.spy( view.element, 'focus' ); + + document.body.appendChild( view.element ); + + view.focus(); + + sinon.assert.calledOnce( focusSpy ); + + view.element.remove(); + } ); + } ); + + function getView( keystrokes ) { + return new AccessibilityHelpContentView( new Locale(), keystrokes ); + } +} ); diff --git a/packages/ckeditor5-ui/tests/formheader/formheaderview.js b/packages/ckeditor5-ui/tests/formheader/formheaderview.js index 486a171b3fd..2b3b023e0f2 100644 --- a/packages/ckeditor5-ui/tests/formheader/formheaderview.js +++ b/packages/ckeditor5-ui/tests/formheader/formheaderview.js @@ -69,6 +69,7 @@ describe( 'FormHeaderView', () => { expect( view.element.firstChild.classList.contains( 'ck' ) ).to.be.true; expect( view.element.firstChild.classList.contains( 'ck-form__header__label' ) ).to.be.true; + expect( view.element.firstChild.role ).to.equal( 'presentation' ); expect( view.element.firstChild.textContent ).to.equal( 'foo' ); view.destroy(); diff --git a/packages/ckeditor5-ui/tests/manual/dialog/dialog.ts b/packages/ckeditor5-ui/tests/manual/dialog/dialog.ts index 5a0228008da..1d15932328e 100644 --- a/packages/ckeditor5-ui/tests/manual/dialog/dialog.ts +++ b/packages/ckeditor5-ui/tests/manual/dialog/dialog.ts @@ -284,6 +284,8 @@ function initEditor( editorName, editorClass, direction = 'ltr', customCallback? ], toolbar: { items: [ + 'accessibilityHelp', + '|', 'heading', 'bold', 'italic', 'link', 'sourceediting', '-', 'findAndReplace', 'modalWithText', 'yesNoModal', ...POSSIBLE_DIALOG_POSITIONS diff --git a/packages/ckeditor5-ui/tests/toolbar/balloon/balloontoolbar.js b/packages/ckeditor5-ui/tests/toolbar/balloon/balloontoolbar.js index a9d46a90bcc..41668fe552e 100644 --- a/packages/ckeditor5-ui/tests/toolbar/balloon/balloontoolbar.js +++ b/packages/ckeditor5-ui/tests/toolbar/balloon/balloontoolbar.js @@ -9,6 +9,7 @@ import BalloonToolbar from '../../../src/toolbar/balloon/balloontoolbar.js'; import ContextualBalloon from '../../../src/panel/balloon/contextualballoon.js'; import BalloonPanelView from '../../../src/panel/balloon/balloonpanelview.js'; import ToolbarView from '../../../src/toolbar/toolbarview.js'; +import ButtonView from '../../../src/button/buttonview.js'; import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker.js'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin.js'; import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold.js'; @@ -793,6 +794,36 @@ describe( 'BalloonToolbar', () => { } ); } ); + describe( 'BalloonToolbar plugin load order', () => { + it( 'should add a button registered in the afterInit of Foo when BalloonToolbar is loaded before Foo', () => { + class Foo extends Plugin { + afterInit() { + this.editor.ui.componentFactory.add( 'foo', () => { + const button = new ButtonView(); + + button.set( { label: 'Foo' } ); + + return button; + } ); + } + } + + return ClassicTestEditor + .create( editorElement, { + plugins: [ BalloonToolbar, Foo ], + balloonToolbar: [ 'foo' ] + } ) + .then( editor => { + const items = editor.plugins.get( BalloonToolbar ).toolbarView.items; + + expect( items.length ).to.equal( 1 ); + expect( items.first.label ).to.equal( 'Foo' ); + + return editor.destroy(); + } ); + } ); + } ); + function stubSelectionRects( rects ) { const originalViewRangeToDom = editingView.domConverter.viewRangeToDom; diff --git a/packages/ckeditor5-ui/tests/toolbar/block/blocktoolbar.js b/packages/ckeditor5-ui/tests/toolbar/block/blocktoolbar.js index d380aacf8ab..992b761fc50 100644 --- a/packages/ckeditor5-ui/tests/toolbar/block/blocktoolbar.js +++ b/packages/ckeditor5-ui/tests/toolbar/block/blocktoolbar.js @@ -13,6 +13,7 @@ import BlockToolbar from '../../../src/toolbar/block/blocktoolbar.js'; import ToolbarView from '../../../src/toolbar/toolbarview.js'; import BalloonPanelView from '../../../src/panel/balloon/balloonpanelview.js'; import BlockButtonView from '../../../src/toolbar/block/blockbuttonview.js'; +import ButtonView from '../../../src/button/buttonview.js'; import Heading from '@ckeditor/ckeditor5-heading/src/heading.js'; import HeadingButtonsUI from '@ckeditor/ckeditor5-heading/src/headingbuttonsui.js'; @@ -23,6 +24,7 @@ import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption.js'; import global from '@ckeditor/ckeditor5-utils/src/dom/global.js'; import ResizeObserver from '@ckeditor/ckeditor5-utils/src/dom/resizeobserver.js'; import DragDropBlockToolbar from '@ckeditor/ckeditor5-clipboard/src/dragdropblocktoolbar.js'; +import Plugin from '@ckeditor/ckeditor5-core/src/plugin.js'; import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model.js'; import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard.js'; @@ -951,4 +953,34 @@ describe( 'BlockToolbar', () => { return multiRootEditor.destroy(); } ); } ); + + describe( 'BlockToolbar plugin load order', () => { + it( 'should add a button registered in the afterInit of Foo when BlockToolbar is loaded before Foo', () => { + class Foo extends Plugin { + afterInit() { + this.editor.ui.componentFactory.add( 'foo', () => { + const button = new ButtonView(); + + button.set( { label: 'Foo' } ); + + return button; + } ); + } + } + + return ClassicTestEditor + .create( element, { + plugins: [ BlockToolbar, Foo ], + blockToolbar: [ 'foo' ] + } ) + .then( editor => { + const items = editor.plugins.get( BlockToolbar ).toolbarView.items; + + expect( items.length ).to.equal( 1 ); + expect( items.first.label ).to.equal( 'Foo' ); + + return editor.destroy(); + } ); + } ); + } ); } ); diff --git a/packages/ckeditor5-ui/theme/components/editorui/accessibilityhelp.css b/packages/ckeditor5-ui/theme/components/editorui/accessibilityhelp.css new file mode 100644 index 00000000000..061e05951c9 --- /dev/null +++ b/packages/ckeditor5-ui/theme/components/editorui/accessibilityhelp.css @@ -0,0 +1,10 @@ +/* + * 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 + */ + +/* + * Note: This file should contain the wireframe styles only. But since there are no such styles, + * it acts as a message to the builder telling that it should look for the corresponding styles + * **in the theme** when compiling the editor. + */ diff --git a/packages/ckeditor5-ui/theme/icons/accessibility.svg b/packages/ckeditor5-ui/theme/icons/accessibility.svg new file mode 100644 index 00000000000..bbd7c7d3354 --- /dev/null +++ b/packages/ckeditor5-ui/theme/icons/accessibility.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/ckeditor5-undo/src/undoediting.ts b/packages/ckeditor5-undo/src/undoediting.ts index 9109ce412e7..b2cd1476e34 100644 --- a/packages/ckeditor5-undo/src/undoediting.ts +++ b/packages/ckeditor5-undo/src/undoediting.ts @@ -7,7 +7,7 @@ * @module undo/undoediting */ -import { Plugin, type Editor } from '@ckeditor/ckeditor5-core'; +import { Plugin } from '@ckeditor/ckeditor5-core'; import UndoCommand, { type UndoCommandRevertEvent } from './undocommand.js'; import RedoCommand from './redocommand.js'; @@ -52,6 +52,7 @@ export default class UndoEditing extends Plugin { */ public init(): void { const editor = this.editor; + const t = editor.t; // Create commands. this._undoCommand = new UndoCommand( editor ); @@ -109,5 +110,19 @@ export default class UndoEditing extends Plugin { editor.keystrokes.set( 'CTRL+Z', 'undo' ); editor.keystrokes.set( 'CTRL+Y', 'redo' ); editor.keystrokes.set( 'CTRL+SHIFT+Z', 'redo' ); + + // Add the information about the keystrokes to the accessibility database. + editor.accessibility.addKeystrokeInfos( { + keystrokes: [ + { + label: t( 'Undo' ), + keystroke: 'CTRL+Z' + }, + { + label: t( 'Redo' ), + keystroke: [ [ 'CTRL+Y' ], [ 'CTRL+SHIFT+Z' ] ] + } + ] + } ); } } diff --git a/packages/ckeditor5-undo/tests/undoediting.js b/packages/ckeditor5-undo/tests/undoediting.js index 5fe7f783e3e..a452f90f335 100644 --- a/packages/ckeditor5-undo/tests/undoediting.js +++ b/packages/ckeditor5-undo/tests/undoediting.js @@ -26,6 +26,22 @@ describe( 'UndoEditing', () => { undo.destroy(); } ); + it( 'should have a name', () => { + expect( UndoEditing.pluginName ).to.equal( 'UndoEditing' ); + } ); + + it( 'should add keystroke accessibility info', () => { + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'common' ).keystrokes ).to.deep.include( { + label: 'Undo', + keystroke: 'CTRL+Z' + } ); + + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'common' ).keystrokes ).to.deep.include( { + label: 'Redo', + keystroke: [ [ 'CTRL+Y' ], [ 'CTRL+SHIFT+Z' ] ] + } ); + } ); + it( 'should register undo command and redo command', () => { expect( editor.commands.get( 'undo' ) ).to.equal( undo._undoCommand ); expect( editor.commands.get( 'redo' ) ).to.equal( undo._redoCommand ); diff --git a/packages/ckeditor5-utils/src/keyboard.ts b/packages/ckeditor5-utils/src/keyboard.ts index deac4b438bc..401b1d3c217 100644 --- a/packages/ckeditor5-utils/src/keyboard.ts +++ b/packages/ckeditor5-utils/src/keyboard.ts @@ -26,6 +26,14 @@ const modifiersToGlyphsNonMac = { shift: 'Shift+' } as const; +const keyCodesToGlyphs: { [key: number]: string } = { + 37: '←', + 38: '↑', + 39: '→', + 40: '↓', + 9: '⇥' +} as const; + /** * An object with `keyName => keyCode` pairs for a set of known keys. * @@ -41,8 +49,18 @@ const modifiersToGlyphsNonMac = { */ export const keyCodes = generateKnownKeyCodes(); -const keyCodeNames = Object.fromEntries( - Object.entries( keyCodes ).map( ( [ name, code ] ) => [ code, name.charAt( 0 ).toUpperCase() + name.slice( 1 ) ] ) +const keyCodeNames: { readonly [ keyCode: number ]: string } = Object.fromEntries( + Object.entries( keyCodes ).map( ( [ name, code ] ) => { + let prettyKeyName; + + if ( code in keyCodesToGlyphs ) { + prettyKeyName = keyCodesToGlyphs[ code ]; + } else { + prettyKeyName = name.charAt( 0 ).toUpperCase() + name.slice( 1 ); + } + + return [ code, prettyKeyName ]; + } ) ); /** @@ -264,9 +282,19 @@ function generateKnownKeyCodes(): { readonly [ keyCode: string ]: number } { } // other characters - for ( const char of '`-=[];\',./\\' ) { - keyCodes[ char ] = char.charCodeAt( 0 ); - } + Object.assign( keyCodes, { + '\'': 222, + ',': 108, + '-': 109, + '.': 110, + '/': 111, + ';': 186, + '=': 187, + '[': 219, + '\\': 220, + ']': 221, + '`': 223 + } ); return keyCodes; } diff --git a/packages/ckeditor5-utils/tests/keyboard.js b/packages/ckeditor5-utils/tests/keyboard.js index 90ad642f7c5..64cb0999d8d 100644 --- a/packages/ckeditor5-utils/tests/keyboard.js +++ b/packages/ckeditor5-utils/tests/keyboard.js @@ -42,6 +42,26 @@ describe( 'Keyboard', () => { '`', '-', '=', '[', ']', ';', '\'', ',', '.', '/', '\\' ); } ); + + it( 'should provide correct codes for interpunction characters, brackets, slashes, etc.', () => { + const charactersToCodes = { + '\'': 222, + ',': 108, + '-': 109, + '.': 110, + '/': 111, + ';': 186, + '=': 187, + '[': 219, + '\\': 220, + ']': 221, + '`': 223 + }; + + for ( const character in charactersToCodes ) { + expect( keyCodes[ character ] ).to.equal( charactersToCodes[ character ] ); + } + } ); } ); describe( 'getCode', () => { @@ -58,7 +78,7 @@ describe( 'Keyboard', () => { } ); it( 'gets code of a punctuation character', () => { - expect( getCode( ']' ) ).to.equal( 93 ); + expect( getCode( ']' ) ).to.equal( 221 ); } ); it( 'is case insensitive', () => { @@ -103,7 +123,7 @@ describe( 'Keyboard', () => { } ); it( 'parses string without modifier', () => { - expect( parseKeystroke( '[' ) ).to.equal( 91 ); + expect( parseKeystroke( '[' ) ).to.equal( 219 ); } ); it( 'allows spacing', () => { @@ -148,7 +168,7 @@ describe( 'Keyboard', () => { } ); it( 'parses string without modifier', () => { - expect( parseKeystroke( '[' ) ).to.equal( 91 ); + expect( parseKeystroke( '[' ) ).to.equal( 219 ); } ); it( 'allows spacing', () => { @@ -192,7 +212,7 @@ describe( 'Keyboard', () => { } ); it( 'parses string without modifier', () => { - expect( parseKeystroke( '[' ) ).to.equal( 91 ); + expect( parseKeystroke( '[' ) ).to.equal( 219 ); } ); it( 'allows spacing', () => { @@ -273,7 +293,7 @@ describe( 'Keyboard', () => { it( 'normalizes value', () => { expect( getEnvKeystrokeText( 'ESC' ) ).to.equal( 'Esc' ); - expect( getEnvKeystrokeText( 'TAB' ) ).to.equal( 'Tab' ); + expect( getEnvKeystrokeText( 'TAB' ) ).to.equal( '⇥' ); expect( getEnvKeystrokeText( 'A' ) ).to.equal( 'A' ); expect( getEnvKeystrokeText( 'a' ) ).to.equal( 'A' ); expect( getEnvKeystrokeText( 'CTRL+a' ) ).to.equal( '⌘A' ); @@ -281,6 +301,13 @@ describe( 'Keyboard', () => { expect( getEnvKeystrokeText( 'CTRL+[' ) ).to.equal( '⌘[' ); expect( getEnvKeystrokeText( 'CTRL+]' ) ).to.equal( '⌘]' ); } ); + + it( 'uses pretty glyphs for arrows', () => { + expect( getEnvKeystrokeText( 'Arrowleft' ) ).to.equal( '←' ); + expect( getEnvKeystrokeText( 'Arrowup' ) ).to.equal( '↑' ); + expect( getEnvKeystrokeText( 'Arrowright' ) ).to.equal( '→' ); + expect( getEnvKeystrokeText( 'Arrowdown' ) ).to.equal( '↓' ); + } ); } ); describe( 'on iOS', () => { @@ -320,7 +347,7 @@ describe( 'Keyboard', () => { it( 'normalizes value', () => { expect( getEnvKeystrokeText( 'ESC' ) ).to.equal( 'Esc' ); - expect( getEnvKeystrokeText( 'TAB' ) ).to.equal( 'Tab' ); + expect( getEnvKeystrokeText( 'TAB' ) ).to.equal( '⇥' ); expect( getEnvKeystrokeText( 'A' ) ).to.equal( 'A' ); expect( getEnvKeystrokeText( 'a' ) ).to.equal( 'A' ); expect( getEnvKeystrokeText( 'CTRL+a' ) ).to.equal( '⌘A' ); @@ -328,6 +355,13 @@ describe( 'Keyboard', () => { expect( getEnvKeystrokeText( 'CTRL+[' ) ).to.equal( '⌘[' ); expect( getEnvKeystrokeText( 'CTRL+]' ) ).to.equal( '⌘]' ); } ); + + it( 'uses pretty glyphs for arrows', () => { + expect( getEnvKeystrokeText( 'Arrowleft' ) ).to.equal( '←' ); + expect( getEnvKeystrokeText( 'Arrowup' ) ).to.equal( '↑' ); + expect( getEnvKeystrokeText( 'Arrowright' ) ).to.equal( '→' ); + expect( getEnvKeystrokeText( 'Arrowdown' ) ).to.equal( '↓' ); + } ); } ); describe( 'on non–Macintosh', () => { @@ -337,7 +371,7 @@ describe( 'Keyboard', () => { it( 'normalizes value', () => { expect( getEnvKeystrokeText( 'ESC' ) ).to.equal( 'Esc' ); - expect( getEnvKeystrokeText( 'TAB' ) ).to.equal( 'Tab' ); + expect( getEnvKeystrokeText( 'TAB' ) ).to.equal( '⇥' ); expect( getEnvKeystrokeText( 'A' ) ).to.equal( 'A' ); expect( getEnvKeystrokeText( 'a' ) ).to.equal( 'A' ); expect( getEnvKeystrokeText( 'CTRL+a' ) ).to.equal( 'Ctrl+A' ); @@ -350,6 +384,13 @@ describe( 'Keyboard', () => { expect( getEnvKeystrokeText( 'CTRL+[' ) ).to.equal( 'Ctrl+[' ); expect( getEnvKeystrokeText( 'CTRL+]' ) ).to.equal( 'Ctrl+]' ); } ); + + it( 'uses pretty glyphs for arrows', () => { + expect( getEnvKeystrokeText( 'Arrowleft' ) ).to.equal( '←' ); + expect( getEnvKeystrokeText( 'Arrowup' ) ).to.equal( '↑' ); + expect( getEnvKeystrokeText( 'Arrowright' ) ).to.equal( '→' ); + expect( getEnvKeystrokeText( 'Arrowdown' ) ).to.equal( '↓' ); + } ); } ); } ); diff --git a/packages/ckeditor5-widget/lang/contexts.json b/packages/ckeditor5-widget/lang/contexts.json index 71615ef972e..5131b3e6c31 100644 --- a/packages/ckeditor5-widget/lang/contexts.json +++ b/packages/ckeditor5-widget/lang/contexts.json @@ -2,5 +2,10 @@ "Widget toolbar": "The label used by assistive technologies describing a toolbar attached to a widget.", "Insert paragraph before block": "The title displayed when a mouse is over a button that inserts a paragraph before a block.", "Insert paragraph after block": "The title displayed when a mouse is over a button that inserts a paragraph after a block.", - "Press Enter to type after or press Shift + Enter to type before the widget": "Information to be read by screen reader about shortcuts to type around a widget" + "Press Enter to type after or press Shift + Enter to type before the widget": "Information to be read by screen reader about shortcuts to type around a widget.", + "Keystrokes that can be used when a widget is selected (for example: image, table, etc.)": "Accessibility help dialog section title for widget plugin keystrokes.", + "Insert a new paragraph directly after a widget": "Accessibility help dialog entry explaining the meaning of the keystroke that inserts a paragraph after a widget.", + "Insert a new paragraph directly before a widget": "Accessibility help dialog entry explaining the meaning of the keystroke that inserts a paragraph before a widget.", + "Move the caret to allow typing directly before a widget": "Accessibility help dialog entry explaining the meaning of the keystroke that moves the caret before a widget.", + "Move the caret to allow typing directly after a widget": "Accessibility help dialog entry explaining the meaning of the keystroke that moves the caret after a widget." } diff --git a/packages/ckeditor5-widget/src/widget.ts b/packages/ckeditor5-widget/src/widget.ts index 3ffcf1c9651..0b985a1ca46 100644 --- a/packages/ckeditor5-widget/src/widget.ts +++ b/packages/ckeditor5-widget/src/widget.ts @@ -80,6 +80,7 @@ export default class Widget extends Plugin { const editor = this.editor; const view = editor.editing.view; const viewDocument = view.document; + const t = editor.t; // Model to view selection converter. // Converts selection placed over widget element to fake selection. @@ -194,6 +195,30 @@ export default class Widget extends Plugin { evt.stop(); } }, { context: '$root' } ); + + // Add the information about the keystrokes to the accessibility database. + editor.accessibility.addKeystrokeInfoGroup( { + id: 'widget', + label: t( 'Keystrokes that can be used when a widget is selected (for example: image, table, etc.)' ), + keystrokes: [ + { + label: t( 'Insert a new paragraph directly after a widget' ), + keystroke: 'Enter' + }, + { + label: t( 'Insert a new paragraph directly before a widget' ), + keystroke: 'Shift+Enter' + }, + { + label: t( 'Move the caret to allow typing directly before a widget' ), + keystroke: [ [ 'arrowup' ], [ 'arrowleft' ] ] + }, + { + label: t( 'Move the caret to allow typing directly after a widget' ), + keystroke: [ [ 'arrowdown' ], [ 'arrowright' ] ] + } + ] + } ); } /** diff --git a/packages/ckeditor5-widget/tests/widget.js b/packages/ckeditor5-widget/tests/widget.js index 0de545ac1e7..31347f28adc 100644 --- a/packages/ckeditor5-widget/tests/widget.js +++ b/packages/ckeditor5-widget/tests/widget.js @@ -137,6 +137,36 @@ describe( 'Widget', () => { return editor.destroy(); } ); + it( 'should have a name', () => { + expect( Widget.pluginName ).to.equal( 'Widget' ); + } ); + + it( 'should add keystroke accessibility info', () => { + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'widget' ).label ).to.equal( + 'Keystrokes that can be used when a widget is selected (for example: image, table, etc.)' + ); + + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'widget' ).keystrokes ).to.deep.include( { + label: 'Insert a new paragraph directly after a widget', + keystroke: 'Enter' + } ); + + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'widget' ).keystrokes ).to.deep.include( { + label: 'Insert a new paragraph directly before a widget', + keystroke: 'Shift+Enter' + } ); + + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'widget' ).keystrokes ).to.deep.include( { + label: 'Move the caret to allow typing directly before a widget', + keystroke: [ [ 'arrowup' ], [ 'arrowleft' ] ] + } ); + + expect( editor.accessibility.keystrokeInfos.get( 'contentEditing' ).groups.get( 'widget' ).keystrokes ).to.deep.include( { + label: 'Move the caret to allow typing directly after a widget', + keystroke: [ [ 'arrowdown' ], [ 'arrowright' ] ] + } ); + } ); + it( 'should be loaded', () => { expect( editor.plugins.get( Widget ) ).to.be.instanceOf( Widget ); } ); diff --git a/scripts/vale/results/2024-02-25--03-51-35.csv b/scripts/vale/results/2024-02-25--03-51-35.csv new file mode 100644 index 00000000000..13787c62a97 --- /dev/null +++ b/scripts/vale/results/2024-02-25--03-51-35.csv @@ -0,0 +1,633 @@ +path,errors,warnings,AutomatedReadability,ColemanLiau,FleschKincaid,FleschReadingEase,GunningFog,LIX,SMOG +CHANGELOG.md,6,240,2.06,4.66,5.28,72.96,6.91,28.32,9.01 +CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +LICENSE.md,0,8,10.61,14.19,10.97,37.97,15.78,53.88,14.01 +README.md,0,28,8.3,11.79,9.59,45.8,12.5,44.12,12.41 +SECURITY.md,0,11,10.44,11.43,11.95,40.39,12.94,51.88,13.35 +docs/api/index.md,0,3,8.26,11.07,8.71,54.94,11.97,41.53,11.91 +docs/examples/builds/balloon-block-editor.md,0,3,8.35,11.71,8.97,50.87,10.54,39.62,11.6 +docs/examples/builds/balloon-editor.md,0,0,6.85,10.64,7.66,55.94,11.28,34.66,10.91 +docs/examples/builds/classic-editor.md,0,0,4.65,8.14,6.46,60.32,10.29,25.73,9.83 +docs/examples/builds/document-editor.md,0,2,5.83,8.8,7.38,61.18,11.35,30.46,11.65 +docs/examples/builds/inline-editor.md,0,3,6.81,10.44,7.73,56.32,11.22,33.38,11.21 +docs/examples/builds/multi-root-editor.md,0,4,6.66,10.02,8.48,52.36,11.94,31.27,11.35 +docs/examples/custom/bottom-toolbar-editor.md,0,5,10.06,12.9,10.58,43.25,11.08,45.6,13.16 +docs/examples/experiments/mermaid.md,0,2,10.16,12.21,10.19,49.07,11.82,43.33,12.24 +docs/examples/framework/content-placeholder.md,0,1,6.2,8.88,7,65.68,8.67,31.67,10.86 +docs/examples/framework/data-from-external-source.md,0,0,6.19,8.58,6.76,68.56,9.26,32.6,10.5 +docs/examples/framework/using-react-in-a-widget.md,0,0,6.53,9.07,7.26,64.69,10.89,34.08,11.21 +docs/examples/how-tos.md,0,30,8.73,8.89,9.72,57.66,12.73,40.19,12.91 +docs/examples/index.md,0,14,7.97,9.6,8.97,57.33,12.02,37.51,12.65 +docs/features/editor-placeholder.md,0,10,6.5,9.59,8.21,55.41,10.45,34.7,11.13 +docs/features/image-upload.md,0,25,8.02,9.93,9.3,53.98,10.7,35.94,12.23 +docs/features/index.md,0,24,8.2,10.03,8.85,57.71,10.57,41.62,11.65 +docs/features/keyboard-support.md,0,1,8.26,11.78,8.73,51.74,8.76,43.21,10.58 +docs/features/math-equations.md,0,33,8.55,11.04,9.36,51.9,10.5,44.68,11.81 +docs/features/productivity-pack.md,0,11,7.32,9.51,8.24,59.91,9.97,38.93,11.02 +docs/features/read-only.md,0,41,6.86,8.4,8.29,61.55,9.64,34.47,11 +docs/features/spelling-and-grammar-checking.md,0,19,10.18,12.59,10.9,42.65,12.77,46.14,13 +docs/features/toolbar.md,0,28,6.33,9.05,7.34,63.13,8.26,36.68,9.67 +docs/features/ui-language.md,0,33,6.51,8.94,7.46,63.74,9.6,38.55,10.89 +docs/features/using-file-managers.md,0,9,6.31,9.97,7.5,57,8.63,29.59,10.22 +docs/framework/architecture/core-editor-architecture.md,0,70,7.21,9.18,8.33,59.97,10.74,37.25,11.54 +docs/framework/architecture/editing-engine.md,0,155,7.87,9.57,8.42,60.99,10.58,39.16,11.54 +docs/framework/architecture/intro.md,0,8,7.47,10.68,8.3,55.34,12.09,39.78,11.7 +docs/framework/architecture/ui-components.md,0,41,3.46,6.66,5.41,69.52,7.81,31.04,8.91 +docs/framework/architecture/ui-library.md,0,73,6.17,8.5,7.52,63.35,9.75,33.89,10.61 +docs/framework/contributing/code-style.md,1,128,6.11,9.55,7.13,60.83,8.78,35.54,10.49 +docs/framework/contributing/contributing.md,0,42,7.55,9.07,8.1,63.64,10.78,35.02,11.31 +docs/framework/contributing/development-environment.md,1,16,8.21,10.32,9.16,54.49,10.91,40.72,11.97 +docs/framework/contributing/git-commit-message-convention.md,0,42,6.97,9.62,7.99,59.47,9.4,40.04,10.7 +docs/framework/contributing/package-metadata.md,0,34,8.76,9.68,9.37,57.69,10.56,42.04,11.63 +docs/framework/contributing/testing-environment.md,0,35,5.97,8.67,6.95,65.62,9.42,32.59,10.59 +docs/framework/custom-editor-creator.md,0,5,6.49,9.17,7.57,61.83,10.7,28.74,10.97 +docs/framework/deep-dive/localization.md,0,60,9.08,10.71,9.7,53.13,11.51,42.15,12.55 +docs/framework/development-tools/inspector.md,0,4,8.12,9.61,9.2,56.39,12.79,40.14,12.94 +docs/framework/development-tools/mrgit.md,0,12,6.79,9.17,8.8,54.65,11.38,38.92,11.86 +docs/framework/development-tools/package-generator/javascript-package.md,0,23,7.29,10.86,7.68,57.76,8.69,41.64,10.51 +docs/framework/development-tools/package-generator/typescript-package.md,0,29,7.25,10.85,7.58,58.25,8.77,41.54,10.45 +docs/framework/development-tools/package-generator/using-package-generator.md,0,7,7.62,10.91,8.59,53.04,9.85,43.23,11.02 +docs/framework/development-tools/testing-helpers.md,0,1,7.42,10.5,7.75,59.88,9.07,35.18,10.91 +docs/framework/index.md,0,41,8.1,11.33,9,50.94,11.19,41.18,12.09 +docs/framework/quick-start.md,0,26,6.81,8.95,8.09,60.62,11.34,37.53,11.94 +docs/index.md,0,3,5.81,8.89,7.15,62.23,10.58,37.98,11.11 +docs/installation/advanced/content-styles.md,0,7,8.04,8.79,8.14,66.46,10.97,36.91,11.29 +docs/installation/advanced/csp.md,0,11,6.69,8.96,7.92,61.26,10.02,33.65,11.42 +docs/installation/advanced/dll-builds-collaboration-features.md,0,6,7.51,9.72,8.55,57.74,10.9,37.5,11.68 +docs/installation/advanced/dll-builds.md,0,16,6.32,8.91,7.11,65.39,9.42,34.58,10.69 +docs/installation/advanced/integrating-from-source-vite.md,0,14,6.67,9.22,7.46,63.4,9.27,34.74,10.78 +docs/installation/advanced/integrating-from-source-webpack.md,0,35,8.06,9.58,8.75,59.38,11.23,39.06,11.91 +docs/installation/advanced/using-two-editors.md,0,21,5.62,7.67,6.26,72.95,9.15,31.21,10.09 +docs/installation/getting-started/api-and-events.md,0,9,5.43,6.94,7.73,64.33,10.96,29.64,11.21 +docs/installation/getting-started/configuration.md,1,18,6.9,9.11,7.87,62.04,10.41,39.14,11.04 +docs/installation/getting-started/editor-lifecycle.md,0,10,9.13,11.02,10.21,48.58,12.93,39.53,12.96 +docs/installation/getting-started/extending-features.md,0,5,6.45,9.45,8.35,54.8,10.14,36.84,10.54 +docs/installation/getting-started/getting-and-setting-data.md,0,8,5.92,7.79,7.82,62.74,10.43,33.57,10.69 +docs/installation/getting-started/predefined-builds.md,0,75,6.43,9.09,8.04,58.47,11,35.76,11.57 +docs/installation/getting-started/quick-start-other.md,0,32,6.48,9.23,7.22,64.04,9.07,35.02,10.5 +docs/installation/getting-started/quick-start.md,0,20,7.78,9.97,8.96,55.19,12.15,38.99,12.23 +docs/installation/index.md,0,10,6.88,8.82,8.01,62.08,11.85,34.92,12.16 +docs/installation/integrations/angular.md,0,48,6.76,9.4,8.2,57.75,11.37,36.25,11.87 +docs/installation/integrations/css.md,0,15,9.65,11.09,10.29,50,13.55,41.14,13.41 +docs/installation/integrations/dotnet.md,0,17,9.77,9.47,9.7,60.17,12.87,41.1,12.3 +docs/installation/integrations/drupal-real-time-collaboration.md,0,15,9.53,11.42,10.3,48.31,11.41,43.2,12.28 +docs/installation/integrations/laravel.md,0,15,10.63,9.95,10.61,55.64,14.06,44.07,13.21 +docs/installation/integrations/next-js.md,0,8,7.4,9.32,7.94,63.14,10.36,34.93,11.12 +docs/installation/integrations/other.md,0,2,10.71,13.18,12.25,33.25,17.35,50.22,15.65 +docs/installation/integrations/overview.md,0,13,8.23,11.77,9.67,44.89,14.34,43.13,12.81 +docs/installation/integrations/react.md,0,54,7.2,9.91,8.07,58.9,10.37,37.56,11.16 +docs/installation/integrations/vuejs-v2.md,0,35,7.34,9.76,8.68,55.88,12.04,38.52,12.31 +docs/installation/integrations/vuejs-v3.md,0,39,7.64,10.12,8.93,54.1,12.2,39.41,12.42 +docs/installation/plugins/features-html-output-overview.md,0,5,6.96,9.86,8.28,56.31,10.1,39.64,11.37 +docs/installation/plugins/installing-plugins.md,1,17,7.76,9.65,8.76,57.68,11.47,40.63,12.18 +docs/installation/plugins/plugins.md,0,6,8.69,11.77,9.75,46.88,10.68,46.58,12.02 +docs/installation/working-with-typescript.md,0,7,8.25,11.4,9.18,50.12,12.92,42.02,12.44 +docs/support/browser-compatibility.md,0,5,7.51,11.46,8.46,50.11,9.4,42.82,10.59 +docs/support/error-codes.md,0,0,8.54,13.01,7.93,50.67,8.51,45.1,9.73 +docs/support/index.md,0,2,9.41,12.3,9.3,51.56,13.92,46.33,13.02 +docs/support/license-and-legal.md,0,6,10.31,12.1,10.67,46.64,13.92,54.51,13.9 +docs/support/license-key-and-activation.md,0,20,7.07,9.33,8.72,55.94,12.36,40.71,12.1 +docs/support/managing-ckeditor-logo.md,0,30,6.24,8.18,8.34,59.07,11.25,34.98,11.75 +docs/support/reporting-issues.md,0,15,6.21,8.85,7.44,62.69,9.08,32.81,10.71 +docs/tutorials/abbreviation-plugin-tutorial/abbreviation-plugin-level-1.md,0,63,7.23,8.53,8.3,62.68,10.06,34.77,10.85 +docs/tutorials/abbreviation-plugin-tutorial/abbreviation-plugin-level-2.md,0,113,5.23,6.17,6.12,77.85,8.25,27.99,8.88 +docs/tutorials/abbreviation-plugin-tutorial/abbreviation-plugin-level-3.md,0,132,7.96,8.51,8.7,63.08,10.44,35.5,11.02 +docs/tutorials/crash-course/commands.md,0,40,7.19,9.35,7.87,62.48,9.19,38.11,11.21 +docs/tutorials/crash-course/data-conversion.md,0,18,8,9.62,8.9,57.87,9.82,37.34,11.21 +docs/tutorials/crash-course/editor.md,0,29,5.42,7.86,6.6,68.73,8.39,31.5,9.95 +docs/tutorials/crash-course/events-and-observables.md,0,19,8.81,10.07,9.22,57.63,10.62,39.82,12.16 +docs/tutorials/crash-course/keystrokes.md,0,24,9.75,9.77,9.89,57.78,10.97,45.07,11.54 +docs/tutorials/crash-course/model-and-schema.md,0,37,5.58,7.41,6.35,73.17,8.01,30.54,10.27 +docs/tutorials/crash-course/plugin-configuration.md,0,22,8.42,9.6,9.47,55.74,11.14,36.38,11.72 +docs/tutorials/crash-course/plugins.md,0,18,6.98,8.62,7.84,64.54,9.93,35.46,10.58 +docs/tutorials/crash-course/view.md,0,24,6.98,8.93,7.75,64.02,9.38,36.28,11.14 +docs/tutorials/creating-simple-plugin-timestamp.md,0,42,6.49,7.61,7.31,69.8,9.61,32.69,10.22 +docs/tutorials/index.md,0,11,9.21,9.5,10.18,54.38,12.84,41.3,13.24 +docs/tutorials/widgets/data-from-external-source.md,0,31,7.62,9.21,8.8,58.41,10.32,37.34,11.49 +docs/tutorials/widgets/implementing-a-block-widget.md,0,71,6.89,8.43,7.94,64.13,9.7,33.95,10.64 +docs/tutorials/widgets/implementing-an-inline-widget.md,0,23,7.54,9.65,8.64,57.51,10.58,36.37,11.3 +docs/tutorials/widgets/using-react-in-a-widget.md,0,30,8.55,9.28,9.04,60.48,11.42,39.1,11.73 +docs/updating/changelog.md,0,0,7.85,10.59,8.64,55.4,10.6,39.5,11.46 +docs/updating/ckeditor4-configuration-compatibility.md,0,33,7.29,9.31,9,55.05,11.92,39.07,12.13 +docs/updating/ckeditor4-plugin-compatibility.md,0,1,7.75,12.01,10.94,30.02,13.67,54.17,12.16 +docs/updating/ckeditor4-troubleshooting.md,0,13,6.65,8.62,7.68,64.07,9.92,36.48,11.1 +docs/updating/index.md,0,9,11.97,12,12.2,43,16,50.69,15.08 +docs/updating/maintaining.md,0,24,10.25,11.03,11.31,45.48,12.96,46.8,13.51 +docs/updating/migration-from-ckeditor-4.md,0,23,9.02,10.88,10.29,47.95,12.91,45.34,13.21 +docs/updating/release-process.md,0,33,11.21,12.06,11.4,45.41,12.39,47.4,13.94 +docs/updating/update-to-25.md,0,25,8.99,11.48,10.06,47.26,12.27,41.65,12.77 +docs/updating/update-to-26.md,0,15,5.71,8.69,6.79,65.15,8.17,34.37,9.96 +docs/updating/update-to-27.md,0,20,7.24,8.71,8.4,61.42,10.67,37.98,11.73 +docs/updating/update-to-28.md,0,5,0.73,0.13,3.64,74.33,5.14,28.76,7.35 +docs/updating/update-to-29.md,0,36,6.43,8.82,8,59.97,8.73,34.73,10.64 +docs/updating/update-to-30.md,0,1,7.53,9.43,9.23,54.04,12.12,39.4,13.02 +docs/updating/update-to-31.md,0,8,5.93,8.03,7.29,65.71,9.03,36.75,11.35 +docs/updating/update-to-32.md,0,34,7.34,9.69,8.81,55.22,11.72,40.84,12.4 +docs/updating/update-to-33.md,0,18,8.38,10.24,9.37,54.04,10.65,41.66,11.45 +docs/updating/update-to-34.md,0,24,9.82,10.36,10.75,49.93,12.8,43.95,13.48 +docs/updating/update-to-35.md,0,52,8.31,10.04,9.22,55.49,10.9,40.59,11.79 +docs/updating/update-to-36.md,0,2,3.62,6.53,6.11,67.44,7.6,32.18,10.13 +docs/updating/update-to-37.md,0,32,7.44,10.44,7.89,59.24,8.69,42.1,10.39 +docs/updating/update-to-38.md,0,16,8.8,9.29,9.8,56.08,12.31,44.6,13.02 +docs/updating/update-to-39.md,0,2,9.03,9.56,10.76,49.25,12.8,42.98,13.56 +docs/updating/update-to-40.md,0,66,7.72,10.36,8.74,54.88,9.42,39.92,11.38 +docs/updating/update-to-41.md,0,48,6.05,8.76,7.04,65.04,8.25,36.18,9.76 +docs/updating/updating-ckeditor-5.md,0,46,8.59,9.48,10.07,52.63,12.93,41.7,13.78 +docs/updating/versioning-policy.md,0,47,10.49,10.9,11.66,44.41,12.55,50.3,13.91 +external/ckeditor5-commercial/docs/examples/builds/collaborative-document-editor.md,0,8,9.76,11.71,10.62,45.95,13.32,43.19,13.69 +external/ckeditor5-commercial/docs/examples/builds/full-featured-editor.md,0,9,10.34,11.15,10.61,50.5,12.05,46.23,12.94 +external/ckeditor5-commercial/docs/features/context-and-collaboration-features.md,0,36,8.79,10.76,9.88,50.35,11.85,43.28,12.1 +external/ckeditor5-commercial/docs/features/users.md,0,22,8.12,10.28,9.74,49.97,10.98,36.82,11.62 +packages/ckeditor5-enter/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-enter/LICENSE.md,0,3,10.44,13.34,10.2,46.12,14.27,56.73,13.48 +packages/ckeditor5-enter/README.md,0,1,5.24,8.89,6.89,58.15,10.96,37.61,9.96 +packages/ckeditor5-enter/docs/api/enter.md,0,1,4.95,8.24,7.41,51.41,8.16,39.26,9.12 +packages/ckeditor5-essentials/CHANGELOG.md,0,28,10.44,15.39,12.19,19.72,13.43,56.25,11.8 +packages/ckeditor5-essentials/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-essentials/LICENSE.md,0,3,10.64,13.59,10.32,45.23,14.69,56.73,13.71 +packages/ckeditor5-essentials/README.md,0,1,5.18,7.78,7.09,51.8,9.69,40.59,8.84 +packages/ckeditor5-essentials/docs/api/essentials.md,0,0,4.38,6.85,6.67,54.91,7.3,39.18,8.55 +packages/ckeditor5-find-and-replace/CHANGELOG.md,0,1,3.94,7.2,7.4,52.7,9.07,39.33,8.84 +packages/ckeditor5-find-and-replace/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-find-and-replace/LICENSE.md,0,4,11.81,14.73,11.9,35.1,16.31,58.75,15.02 +packages/ckeditor5-find-and-replace/README.md,0,1,5.01,8.58,6.41,62.45,9.67,37.5,9.52 +packages/ckeditor5-find-and-replace/docs/api/find-and-replace.md,0,0,3.07,5.55,4.57,70.66,6.99,32.36,7.55 +packages/ckeditor5-find-and-replace/docs/features/find-and-replace.md,0,7,4.96,8,6.35,67.04,7.42,34.19,9.04 +packages/ckeditor5-font/CHANGELOG.md,0,11,8.3,12.45,7.62,50.02,9.6,47.77,9.81 +packages/ckeditor5-font/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-font/LICENSE.md,0,7,10.1,13.17,10.52,42.78,14.71,52.86,13.89 +packages/ckeditor5-font/README.md,0,1,4.73,8.04,6.04,61.53,8.87,34.29,8.84 +packages/ckeditor5-font/docs/api/font.md,0,0,2.5,3.16,4.3,70.23,7.3,26.76,7.17 +packages/ckeditor5-font/docs/features/font.md,0,42,6.21,9.34,7.33,61.19,8.8,34.94,10.31 +packages/ckeditor5-heading/CHANGELOG.md,0,23,7.42,11.47,7.38,52.32,8.41,48.18,9.54 +packages/ckeditor5-heading/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-heading/LICENSE.md,0,3,10.59,13.52,10.2,46.12,14.27,57.78,13.48 +packages/ckeditor5-heading/README.md,0,1,5.28,8.95,6.74,59.23,9.94,39.14,9.52 +packages/ckeditor5-heading/docs/api/heading.md,0,0,3.25,4.22,5.31,63.05,7.22,35.12,7.49 +packages/ckeditor5-heading/docs/features/headings.md,0,24,6.62,9.88,7.55,59.4,8.81,39.1,10.1 +packages/ckeditor5-heading/docs/features/title.md,0,8,6,8.71,7.7,60.26,9.07,33.89,10.21 +packages/ckeditor5-highlight/CHANGELOG.md,0,11,9.68,13.48,8.94,38.62,11.68,57.9,10.1 +packages/ckeditor5-highlight/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-highlight/LICENSE.md,0,3,10.64,13.59,10.2,46.12,14.27,57.78,13.48 +packages/ckeditor5-highlight/README.md,0,1,5.62,9.37,6.74,59.23,9.94,39.14,9.52 +packages/ckeditor5-highlight/docs/api/highlight.md,0,0,3.4,5.04,5.14,64.95,7.84,32.75,7.55 +packages/ckeditor5-highlight/docs/features/highlight.md,0,10,6.02,9.53,7.07,60.66,8.66,35.15,9.83 +packages/ckeditor5-horizontal-line/CHANGELOG.md,0,2,10.28,13.89,10.93,23.81,12.37,61.69,10.04 +packages/ckeditor5-horizontal-line/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-horizontal-line/LICENSE.md,0,3,10.69,13.59,10.41,44.87,14.65,57.46,13.71 +packages/ckeditor5-horizontal-line/README.md,0,1,5.89,9.54,7.4,56.91,11.11,38.23,10.41 +packages/ckeditor5-horizontal-line/docs/api/horizontal-line.md,0,0,3.47,5.26,5.6,61.88,8.74,32.11,7.91 +packages/ckeditor5-horizontal-line/docs/features/horizontal-line.md,0,9,5.6,9.02,7.6,56.74,9.45,35.95,10.26 +packages/ckeditor5-html-embed/CHANGELOG.md,0,1,3.94,7.2,7.4,52.7,9.07,39.33,8.84 +packages/ckeditor5-html-embed/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-html-embed/LICENSE.md,0,3,10.45,13.28,10.16,46.63,14.24,56.42,13.48 +packages/ckeditor5-html-embed/README.md,0,1,5.19,8.6,6.85,61.38,9.62,34.31,9.99 +packages/ckeditor5-html-embed/docs/api/html-embed.md,0,0,2.86,4.5,4.99,66.21,7.71,29.54,7.55 +packages/ckeditor5-html-embed/docs/features/html-embed.md,0,23,8.68,11.04,9.56,51.03,11.01,43.33,12.05 +packages/ckeditor5-html-support/CHANGELOG.md,0,1,3.94,7.2,7.4,52.7,9.07,39.33,8.84 +packages/ckeditor5-html-support/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-html-support/LICENSE.md,0,4,11.85,14.84,11.93,34.64,16.35,58.99,15.02 +packages/ckeditor5-html-support/README.md,0,1,6.16,9.68,7.48,57.83,10.92,40.55,10.75 +packages/ckeditor5-html-support/docs/api/html-support.md,0,0,2.14,3.6,4.39,70.55,7.71,32.11,7.55 +packages/ckeditor5-html-support/docs/features/full-page-html.md,0,2,5.78,9.14,7,61.63,8.01,36.24,10.31 +packages/ckeditor5-html-support/docs/features/general-html-support.md,0,27,7.34,9.54,8.62,57.16,10.22,40.63,11.57 +packages/ckeditor5-html-support/docs/features/html-comments.md,0,7,7.19,9.88,8.28,57.45,9.58,40.33,11.12 +packages/ckeditor5-image/CHANGELOG.md,0,1,5.46,8.91,6.26,59.72,8.66,40.26,9.19 +packages/ckeditor5-image/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-image/LICENSE.md,0,4,11.77,14.8,11.97,34.17,16.39,58.44,15.02 +packages/ckeditor5-image/README.md,0,2,5.27,8.87,7.17,57.54,9.97,37.78,9.99 +packages/ckeditor5-image/docs/api/image.md,0,1,2.27,2.59,4.99,65,6.81,32.53,7.39 +packages/ckeditor5-image/docs/features/images-captions.md,0,3,4.43,7.16,6.23,68.82,7.43,31.61,9.12 +packages/ckeditor5-image/docs/features/images-inserting.md,0,12,5.28,7.54,7.22,64.88,8.05,30.12,10.09 +packages/ckeditor5-image/docs/features/images-installation.md,0,25,7.79,10.27,9.55,49.86,9.67,39.14,11.86 +packages/ckeditor5-image/docs/features/images-linking.md,0,6,3.37,5.96,5.96,70.11,6.92,26.15,9.16 +packages/ckeditor5-image/docs/features/images-overview.md,0,23,9.35,11.25,10.25,48.46,10.55,42.12,12.3 +packages/ckeditor5-image/docs/features/images-resizing.md,0,25,7.1,8.81,8.54,59.36,9.75,34.64,11.08 +packages/ckeditor5-image/docs/features/images-responsive.md,0,16,8.25,10.47,9.12,54.32,10.3,37.28,11.89 +packages/ckeditor5-image/docs/features/images-styles.md,0,42,8.31,10.39,9.32,53.47,10.41,37.2,11.83 +packages/ckeditor5-image/docs/features/images-text-alternative.md,0,5,6.36,9.8,8.52,51.24,10.63,33.63,11.12 +packages/ckeditor5-image/docs/framework/deep-dive/upload-adapter.md,0,62,8.49,9.47,9.49,56.34,11.37,36.4,12.24 +packages/ckeditor5-indent/CHANGELOG.md,0,7,10.01,14.75,10.24,32.17,12.5,53.98,10.97 +packages/ckeditor5-indent/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-indent/LICENSE.md,0,3,10.79,13.71,10.41,44.87,14.65,57.46,13.71 +packages/ckeditor5-indent/README.md,0,1,6.1,9.96,7.48,54.15,11.27,38.7,10.13 +packages/ckeditor5-indent/docs/api/indent.md,0,0,3.65,5.35,5.77,60.49,8.89,32.75,7.91 +packages/ckeditor5-indent/docs/features/indent.md,0,26,7.49,10.68,8.08,56.99,9.52,40.09,10.7 +packages/ckeditor5-language/CHANGELOG.md,0,1,3.94,7.2,7.4,52.7,9.07,39.33,8.84 +packages/ckeditor5-language/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-language/LICENSE.md,0,3,10.6,13.4,10.13,47.12,14.2,57.16,13.48 +packages/ckeditor5-language/README.md,0,3,5.96,9.26,6.65,64.81,9.23,40.41,9.89 +packages/ckeditor5-language/docs/api/language.md,0,2,3.19,5.97,4.61,71.19,6.5,35.7,7.96 +packages/ckeditor5-language/docs/features/language.md,0,9,7.05,10.44,7.72,58.11,9.05,40.9,10.3 +packages/ckeditor5-link/CHANGELOG.md,0,33,6.74,10.73,6.39,60.31,8.12,42.02,9.43 +packages/ckeditor5-link/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-link/LICENSE.md,0,4,11.74,14.75,11.88,34.84,16.39,58.44,15.02 +packages/ckeditor5-link/README.md,0,1,5.09,8.57,6.75,61.3,9.23,33.9,9.99 +packages/ckeditor5-link/docs/api/link.md,0,0,2.97,5.79,5.3,66.6,6.89,27.4,8.3 +packages/ckeditor5-link/docs/features/link.md,0,14,7.96,10.11,9.15,54.08,11.24,37.51,12.16 +packages/ckeditor5-list/CHANGELOG.md,0,24,6.7,10.71,6.99,56.76,8.64,41.98,9.74 +packages/ckeditor5-list/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-list/LICENSE.md,0,3,10.39,13.28,10.07,47.01,14.27,56.73,13.48 +packages/ckeditor5-list/README.md,0,1,5.5,8.94,7.32,58.42,10.31,38.45,10.69 +packages/ckeditor5-list/docs/api/list.md,0,0,2.26,4.68,4.62,70.65,6.86,29.39,7.91 +packages/ckeditor5-list/docs/features/lists-editing.md,0,10,7.92,10.1,8.93,55.54,9.55,37.65,10.91 +packages/ckeditor5-list/docs/features/lists-installation.md,0,6,5.91,8.93,7.56,59.73,8.61,36.95,10.54 +packages/ckeditor5-list/docs/features/lists.md,0,5,4.96,7.87,5.95,70.61,6.43,32.56,9.21 +packages/ckeditor5-list/docs/features/todo-lists.md,0,0,4.02,6.81,5.81,70.98,6.99,30.76,9.02 +packages/ckeditor5-markdown-gfm/CHANGELOG.md,0,27,13.28,18.89,15.35,-1.45,16.64,64.37,13.54 +packages/ckeditor5-markdown-gfm/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-markdown-gfm/LICENSE.md,0,5,10.18,13.67,9.8,46.35,13.26,56.45,13.02 +packages/ckeditor5-markdown-gfm/README.md,0,1,7.87,11.14,8.41,54.72,10.03,41.92,10.75 +packages/ckeditor5-markdown-gfm/docs/api/markdown-gfm.md,0,0,6.55,10.53,7.63,52.84,8.51,38.87,9.47 +packages/ckeditor5-markdown-gfm/docs/features/markdown.md,0,22,9.2,11.57,9.48,52.09,10.41,42.55,11.59 +packages/ckeditor5-markdown-gfm/docs/features/paste-markdown.md,0,3,6.29,9.26,6.95,64.78,7.87,35.02,9.91 +packages/ckeditor5-media-embed/CHANGELOG.md,0,10,7.24,11.33,7.24,54.08,9.69,45.41,9.87 +packages/ckeditor5-media-embed/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-media-embed/LICENSE.md,0,3,10.5,13.34,10.41,44.87,14.65,56.42,13.71 +packages/ckeditor5-media-embed/README.md,0,1,5.2,8.59,7.7,55.5,11.61,32.83,10.75 +packages/ckeditor5-media-embed/docs/api/media-embed.md,0,0,3.12,6.02,6.01,61.71,9.52,27.12,8.84 +packages/ckeditor5-media-embed/docs/features/media-embed.md,0,44,8.16,10.53,9.58,50.39,12.09,39.9,12.55 +packages/ckeditor5-mention/CHANGELOG.md,0,14,7.22,11.35,7.77,50.72,9.39,46.24,9.69 +packages/ckeditor5-mention/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-mention/LICENSE.md,0,4,11.85,14.89,11.97,34.17,16.39,59.24,15.02 +packages/ckeditor5-mention/README.md,0,1,6.78,10.73,8.18,50.67,11.33,39.25,10.41 +packages/ckeditor5-mention/docs/api/mention.md,0,0,4.62,7.45,6.58,56.15,8.13,34.05,8 +packages/ckeditor5-mention/docs/examples/chat-with-mentions.md,0,1,7.77,10.15,9.06,53.72,12.47,34,12.38 +packages/ckeditor5-mention/docs/features/mentions.md,0,43,6.98,9.81,8.33,56.26,9.81,38.79,10.87 +packages/ckeditor5-minimap/CHANGELOG.md,0,1,3.94,7.2,7.4,52.7,9.07,39.33,8.84 +packages/ckeditor5-minimap/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-minimap/LICENSE.md,0,3,10.54,13.46,10.32,45.23,14.69,57.78,13.71 +packages/ckeditor5-minimap/README.md,0,1,5.96,9.66,7.47,56.18,11.18,43.1,10.41 +packages/ckeditor5-minimap/docs/api/minimap.md,0,0,3.15,4.73,5.46,62.72,8.89,32.75,7.91 +packages/ckeditor5-minimap/docs/features/minimap.md,0,14,6.31,8.85,7.73,61.13,11.24,38.27,11.4 +packages/ckeditor5-page-break/CHANGELOG.md,0,1,9.72,12.32,9.59,32.39,11.37,61.76,9.42 +packages/ckeditor5-page-break/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-page-break/LICENSE.md,0,3,10.45,13.28,10.04,47.51,14.24,56.42,13.48 +packages/ckeditor5-page-break/README.md,0,1,4.83,8.22,5.82,68.28,9.32,33.75,9.52 +packages/ckeditor5-page-break/docs/api/page-break.md,0,0,2.86,4.5,4.69,68.38,7.71,29.54,7.55 +packages/ckeditor5-page-break/docs/features/page-break.md,0,5,5.74,8.27,6.96,66.06,8.72,33.43,10.02 +packages/ckeditor5-paragraph/CHANGELOG.md,0,30,10.33,15.24,11.23,27.47,12.38,53.82,11.6 +packages/ckeditor5-paragraph/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-paragraph/LICENSE.md,0,3,10.64,13.59,10.32,45.23,14.69,57.78,13.71 +packages/ckeditor5-paragraph/README.md,0,1,5.92,9.73,7.75,51.2,12.19,41.35,10.29 +packages/ckeditor5-paragraph/docs/api/paragraph.md,0,0,4.52,6.24,6.58,54.42,9.95,40.03,8.17 +packages/ckeditor5-paste-from-office/CHANGELOG.md,1,8,8.8,13.34,9.13,41.83,10.68,45.65,10.44 +packages/ckeditor5-paste-from-office/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-paste-from-office/LICENSE.md,0,3,10.55,13.34,10.13,47.12,14.2,56.13,13.48 +packages/ckeditor5-paste-from-office/README.md,0,1,6.64,10.45,7.06,59.71,9.14,35.83,9.73 +packages/ckeditor5-paste-from-office/docs/api/paste-from-office.md,0,0,4.15,5.94,6.01,58.68,8.57,33.19,7.79 +packages/ckeditor5-paste-from-office/docs/features/paste-from-google-docs.md,0,13,8.85,11.31,8.83,56.05,9.39,41.02,10.99 +packages/ckeditor5-paste-from-office/docs/features/paste-from-office.md,0,22,9.06,11.41,9.15,54.36,10.11,41.29,11.56 +packages/ckeditor5-remove-format/CHANGELOG.md,0,5,11.54,15.58,11.44,20.32,12.75,65.2,10.33 +packages/ckeditor5-remove-format/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-remove-format/LICENSE.md,0,3,10.6,13.46,10.29,45.75,14.24,56.42,13.48 +packages/ckeditor5-remove-format/README.md,0,1,5.77,9.55,7.27,55.63,9.87,36.95,9.83 +packages/ckeditor5-remove-format/docs/api/remove-format.md,0,0,3.22,4.95,5.3,64.05,7.71,29.54,7.55 +packages/ckeditor5-remove-format/docs/features/remove-format.md,0,12,7.28,9.51,8.29,59.34,9.62,35.63,11.27 +packages/ckeditor5-restricted-editing/CHANGELOG.md,0,7,8.38,12.74,8.67,46.89,10.85,48.95,11.38 +packages/ckeditor5-restricted-editing/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-restricted-editing/LICENSE.md,0,3,10.84,13.77,10.53,43.98,14.24,58.51,13.93 +packages/ckeditor5-restricted-editing/README.md,0,1,6.52,10.33,7.93,53.13,9.32,42.7,11.21 +packages/ckeditor5-restricted-editing/docs/api/restricted-editing.md,0,0,3.32,5.43,5.43,63.59,7.39,35.15,8.24 +packages/ckeditor5-restricted-editing/docs/features/restricted-editing.md,0,13,8.37,10.5,9.51,51.97,10.16,42.03,12.4 +packages/ckeditor5-select-all/CHANGELOG.md,0,1,5.81,9.57,8.09,48.21,7.3,42.25,9.52 +packages/ckeditor5-select-all/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-select-all/LICENSE.md,0,3,10.45,13.28,10.16,46.63,14.24,56.42,13.48 +packages/ckeditor5-select-all/README.md,0,1,4.88,8.33,6.46,63.23,9.4,34.28,9.52 +packages/ckeditor5-select-all/docs/api/select-all.md,0,1,4.55,7.68,6.93,54.64,7.69,35.01,8.84 +packages/ckeditor5-select-all/docs/features/select-all.md,0,5,6.73,8.92,8.07,60.5,9.21,37.15,10.36 +packages/ckeditor5-show-blocks/CHANGELOG.md,0,1,4.28,7.69,8.73,44.05,11.83,44.96,10.13 +packages/ckeditor5-show-blocks/LICENSE.md,0,3,10.5,13.34,10.04,47.51,14.24,56.42,13.48 +packages/ckeditor5-show-blocks/README.md,0,1,5.9,9.15,6.3,67.52,8.45,35.73,9.73 +packages/ckeditor5-show-blocks/docs/api/show-blocks.md,0,0,4.72,7.98,5.3,66.6,6.89,35.87,8 +packages/ckeditor5-show-blocks/docs/features/show-blocks.md,0,5,6.85,9.22,7.7,62.61,8.74,38.15,10.36 +packages/ckeditor5-source-editing/CHANGELOG.md,0,1,3.94,7.2,7.4,52.7,9.07,39.33,8.84 +packages/ckeditor5-source-editing/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-source-editing/LICENSE.md,0,3,10.65,13.52,10.29,45.75,14.24,57.46,13.71 +packages/ckeditor5-source-editing/README.md,0,1,5.81,9.04,6.93,62.95,9.52,36.46,10.69 +packages/ckeditor5-source-editing/docs/api/source-editing.md,0,0,3.35,5.11,5.3,64.05,7.71,32.11,7.91 +packages/ckeditor5-source-editing/docs/features/source-editing.md,0,42,8.35,10.11,9.21,55.53,10.55,38,12.1 +packages/ckeditor5-special-characters/CHANGELOG.md,0,2,6.72,10.3,7.6,49.48,10.62,51.15,9.66 +packages/ckeditor5-special-characters/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-special-characters/LICENSE.md,0,3,10.84,13.77,10.41,44.87,14.65,58.51,13.71 +packages/ckeditor5-special-characters/README.md,0,1,6.53,10.37,7.47,56.18,11.18,43.1,10.41 +packages/ckeditor5-special-characters/docs/api/special-characters.md,0,0,3.83,5.71,5.6,61.88,8.74,34.67,7.91 +packages/ckeditor5-special-characters/docs/features/special-characters.md,0,18,8.16,11.41,9.93,44.26,11.84,42.96,11.93 +packages/ckeditor5-style/CHANGELOG.md,0,1,3.94,7.2,7.4,52.7,9.07,39.33,8.84 +packages/ckeditor5-style/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-style/LICENSE.md,0,3,10.44,13.34,10.07,47.01,14.27,56.73,13.48 +packages/ckeditor5-style/README.md,0,1,5.39,9.04,7.11,57.63,10.92,37.13,10.13 +packages/ckeditor5-style/docs/api/style.md,0,0,3.86,6.13,5.31,64.52,8,33.17,7.79 +packages/ckeditor5-style/docs/features/style.md,0,7,6.33,9.86,7.47,58.16,9.36,33.81,10.23 +packages/ckeditor5-table/CHANGELOG.md,0,35,7.62,11.87,7.5,53.69,8.8,42.12,9.79 +packages/ckeditor5-table/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-table/LICENSE.md,0,4,11.77,14.8,11.97,34.17,16.39,58.44,15.02 +packages/ckeditor5-table/README.md,0,1,4.26,7.58,5.41,66.77,7.64,32.3,8.52 +packages/ckeditor5-table/docs/api/table.md,0,0,2.28,-1.26,4.53,66.4,6.72,31.43,6.6 +packages/ckeditor5-table/docs/features/tables-caption.md,0,9,5.15,8.09,6.69,65.39,7.47,33.72,9.22 +packages/ckeditor5-table/docs/features/tables-resize.md,0,7,6.46,9.22,7.68,60.66,8.49,33.92,9.92 +packages/ckeditor5-table/docs/features/tables-styling.md,0,12,7.04,9.87,7.73,60.65,8.11,34.06,10.06 +packages/ckeditor5-table/docs/features/tables.md,0,59,7.54,9.45,8.78,57.33,9.42,36.13,10.68 +packages/ckeditor5-theme-lark/CHANGELOG.md,0,58,7.94,11.96,7.86,54.72,9.95,41.12,10.75 +packages/ckeditor5-theme-lark/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-theme-lark/LICENSE.md,0,3,10.29,13.15,9.95,47.9,14.27,55.68,13.48 +packages/ckeditor5-theme-lark/README.md,0,1,5.55,9.28,7,57.11,11.08,38.11,9.96 +packages/ckeditor5-theme-lark/docs/api/theme-lark.md,0,0,2.21,3.52,3.93,73.61,7.39,30.25,7.39 +packages/ckeditor5-theme-lark/docs/examples/theme-customization.md,0,2,6.12,9.9,7.69,54.19,11.53,30.92,10.86 +packages/ckeditor5-theme-lark/docs/framework/theme-customization.md,0,19,8.86,10.18,9.09,58.41,10.4,39,11.76 +packages/ckeditor5-typing/CHANGELOG.md,0,33,6.69,10.71,7.18,55.66,8.61,41.29,9.63 +packages/ckeditor5-typing/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-typing/LICENSE.md,0,4,11.81,14.85,11.97,34.17,16.39,58.44,15.02 +packages/ckeditor5-typing/README.md,0,1,8.81,13.08,9.64,41.73,11.44,45.49,11.46 +packages/ckeditor5-typing/docs/api/typing.md,0,1,8.17,12.55,9.57,38.61,9.13,44.15,10.44 +packages/ckeditor5-typing/docs/features/text-transformation.md,0,7,8.69,11.97,9.02,51.18,10.38,38.23,11.56 +packages/ckeditor5-ui/CHANGELOG.md,0,77,7.03,11.11,7.06,55.82,8.94,40.42,9.51 +packages/ckeditor5-ui/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-ui/LICENSE.md,0,4,11.74,14.75,11.97,34.17,16.71,58.44,15.17 +packages/ckeditor5-ui/README.md,0,1,6.25,10.05,8.46,48.91,12.48,40.43,10.95 +packages/ckeditor5-ui/docs/api/ui.md,0,0,3.96,7.27,6.11,62.6,9.4,32.08,9.1 +packages/ckeditor5-ui/docs/examples/custom-ui.md,0,2,6.31,9.97,8.49,49.85,12.61,36.07,11.98 +packages/ckeditor5-ui/docs/features/blocktoolbar.md,0,11,7.01,8.39,7.8,65.77,9.47,36.22,10.3 +packages/ckeditor5-ui/docs/framework/deep-dive/focus-tracking.md,0,100,8.72,8.96,9.53,58.79,11.32,38.49,11.76 +packages/ckeditor5-ui/docs/framework/external-ui.md,0,17,7.86,8.99,8.53,62.22,10.71,38.17,11.42 +packages/ckeditor5-undo/CHANGELOG.md,0,16,9.1,13.02,8.8,40.17,10.99,54.92,9.98 +packages/ckeditor5-undo/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-undo/LICENSE.md,0,3,10.39,13.28,10.2,46.12,14.27,56.73,13.48 +packages/ckeditor5-undo/README.md,0,1,5.41,9.1,7.49,53.03,11.32,39.18,9.96 +packages/ckeditor5-undo/docs/api/undo.md,0,1,5.37,8.62,8.07,46.1,8.4,39,9.12 +packages/ckeditor5-undo/docs/features/undo-redo.md,0,19,7.34,9.52,8.53,57.84,9.47,38.55,10.69 +packages/ckeditor5-upload/CHANGELOG.md,0,34,6.38,10.27,7.23,54.21,8.45,41.84,9.48 +packages/ckeditor5-upload/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-upload/LICENSE.md,0,3,10.49,13.4,10.2,46.12,14.27,56.73,13.48 +packages/ckeditor5-upload/README.md,0,1,6.04,9.9,8.23,48.3,12.74,40.19,10.61 +packages/ckeditor5-upload/docs/api/upload.md,0,0,5.64,9.28,8.77,42.59,9.19,34.4,9.93 +packages/ckeditor5-upload/docs/features/base64-upload-adapter.md,0,14,6.94,9.11,8.84,55.26,9.57,34.34,11.81 +packages/ckeditor5-upload/docs/features/simple-upload-adapter.md,0,14,7.49,9.21,9.01,56.3,10.65,35.56,11.99 +packages/ckeditor5-utils/CHANGELOG.md,0,38,7.53,11.75,7.89,50.1,9.98,43.14,9.94 +packages/ckeditor5-utils/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-utils/LICENSE.md,0,4,11.82,14.9,12.11,33.01,16.76,58.69,15.17 +packages/ckeditor5-utils/README.md,0,1,4.69,8.22,7.58,53.19,12.09,39.14,10.41 +packages/ckeditor5-utils/docs/api/utils.md,0,0,2.47,5.22,5.86,62.82,8.75,30.95,8.55 +packages/ckeditor5-utils/docs/framework/deep-dive/observables.md,0,73,8.2,9.48,9.03,58.4,11.01,39.37,11.98 +packages/ckeditor5-watchdog/CHANGELOG.md,0,13,8.04,12.31,9.09,43.93,9.78,43.8,10.57 +packages/ckeditor5-watchdog/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-watchdog/LICENSE.md,0,4,11.89,14.94,11.97,34.17,16.39,59.24,15.02 +packages/ckeditor5-watchdog/README.md,0,1,5.68,9.35,7.07,58.6,10.7,40.81,10.13 +packages/ckeditor5-watchdog/docs/api/watchdog.md,0,0,3.74,6.24,5.83,61.27,8.91,35.61,8.24 +packages/ckeditor5-watchdog/docs/features/watchdog.md,0,18,8.66,10.71,9.42,53.2,11.59,41.11,12.07 +packages/ckeditor5-widget/CHANGELOG.md,0,33,7.67,11.88,8.03,51.18,10.12,44,10.31 +packages/ckeditor5-widget/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-widget/LICENSE.md,0,4,11.66,14.66,11.97,34.17,16.39,57.65,15.02 +packages/ckeditor5-widget/README.md,0,1,5.08,8.69,7.36,54.22,11.2,36.5,9.96 +packages/ckeditor5-widget/docs/api/widget.md,0,9,5.62,8.9,8,54.76,9.33,35.66,10.54 +packages/ckeditor5-widget/docs/framework/deep-dive/widget-internals.md,0,20,10.11,10.4,10.36,53.8,11.82,43.93,12.01 +packages/ckeditor5-word-count/CHANGELOG.md,0,5,9.23,13.46,9.37,36.89,10.29,53.32,9.97 +packages/ckeditor5-word-count/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-word-count/LICENSE.md,0,4,11.92,14.82,11.95,34.89,16.59,58.52,15.17 +packages/ckeditor5-word-count/README.md,0,1,5.68,9.37,6.59,61.87,10.77,36.45,10.13 +packages/ckeditor5-word-count/docs/api/word-count.md,0,1,2.89,5.62,4.65,70.96,8.39,26.23,8.3 +packages/ckeditor5-word-count/docs/features/word-count.md,0,17,6.89,9.55,7.69,61.49,9.47,35.72,10.62 +external/ckeditor5-commercial/packages/ckeditor5-import-word/LICENSE.md,0,7,10.39,12.73,9.85,50.63,13.26,55.49,13.26 +external/ckeditor5-commercial/packages/ckeditor5-import-word/README.md,0,13,8.27,10.8,8.69,56.28,11.49,46.53,12.16 +external/ckeditor5-commercial/packages/ckeditor5-import-word/docs/api/import-word.md,0,0,2.62,5.41,4.52,72.43,6.75,28.23,7.65 +external/ckeditor5-commercial/packages/ckeditor5-import-word/docs/features/features-comparison.md,0,57,6.03,9.79,7.3,56.96,8.62,37.98,10.14 +external/ckeditor5-commercial/packages/ckeditor5-import-word/docs/features/import-word.md,0,45,8.03,10.04,8.67,58.14,10.09,40.73,11.54 +external/ckeditor5-commercial/packages/ckeditor5-operations-compressor/CHANGELOG.md,0,14,15.33,21.4,17.13,-13.57,17.75,67.55,14.02 +external/ckeditor5-commercial/packages/ckeditor5-operations-compressor/LICENSE.md,0,9,12.26,15.32,12.56,30.29,17.21,59.69,15.79 +external/ckeditor5-commercial/packages/ckeditor5-operations-compressor/README.md,0,11,11.77,14.38,12.8,29.75,16.17,56.57,15.11 +external/ckeditor5-commercial/packages/ckeditor5-pagination/CHANGELOG.md,0,1,4.28,7.69,8.73,44.05,11.83,44.96,10.13 +external/ckeditor5-commercial/packages/ckeditor5-pagination/LICENSE.md,0,8,12.31,15.2,12.61,30.61,16.81,61.88,15.47 +external/ckeditor5-commercial/packages/ckeditor5-pagination/README.md,0,15,8.58,10.98,9.55,50.87,12.79,48.97,12.8 +external/ckeditor5-commercial/packages/ckeditor5-pagination/docs/api/pagination.md,0,0,2.69,5.66,4.39,74.5,6.81,26.11,8.17 +external/ckeditor5-commercial/packages/ckeditor5-pagination/docs/features/pagination-integration.md,0,40,8.89,10.06,9.1,58.84,11.43,38.34,11.73 +external/ckeditor5-commercial/packages/ckeditor5-pagination/docs/features/pagination.md,0,25,7.58,10.22,8.8,54.39,11.02,39.99,11.49 +external/ckeditor5-commercial/packages/ckeditor5-paste-from-office-enhanced/CHANGELOG.md,0,1,4.28,7.69,8.73,44.05,11.83,44.96,10.13 +external/ckeditor5-commercial/packages/ckeditor5-paste-from-office-enhanced/LICENSE.md,0,7,11.11,13.42,10.13,49.4,13.23,57.6,13.26 +external/ckeditor5-commercial/packages/ckeditor5-paste-from-office-enhanced/README.md,0,11,9.78,12.46,9.36,52.31,11.97,51.88,12.16 +external/ckeditor5-commercial/packages/ckeditor5-paste-from-office-enhanced/docs/api/paste-from-office-enhanced.md,0,0,3.5,6.13,4.89,68.43,7.16,31.07,7.65 +external/ckeditor5-commercial/packages/ckeditor5-paste-from-office-enhanced/docs/features/paste-from-office-enhanced.md,0,26,8.69,11.75,9.03,52.05,10.45,41.1,11.63 +external/ckeditor5-commercial/packages/ckeditor5-real-time-collaboration/CHANGELOG.md,1,49,9.96,13.46,11.2,36.02,11.32,44.8,12.39 +external/ckeditor5-commercial/packages/ckeditor5-real-time-collaboration/LICENSE.md,1,10,11.76,14.78,12.28,31.99,15.84,55.67,14.67 +external/ckeditor5-commercial/packages/ckeditor5-real-time-collaboration/README.md,0,11,10.59,13.7,11.77,34.17,14.67,51.08,13.74 +external/ckeditor5-commercial/packages/ckeditor5-real-time-collaboration/docs/features/real-time-collaboration-integration.md,0,66,9.58,10.66,10.41,50.36,12.87,42.68,13.09 +external/ckeditor5-commercial/packages/ckeditor5-real-time-collaboration/docs/features/real-time-collaboration.md,0,14,9.16,11.56,10.46,44.85,12.11,43.14,12.62 +external/ckeditor5-commercial/packages/ckeditor5-real-time-collaboration/docs/features/users-in-real-time-collaboration.md,0,37,8.35,9.79,9.82,52.25,11.69,38.34,12.23 +external/ckeditor5-commercial/packages/ckeditor5-revision-history/CHANGELOG.md,0,1,4.35,7.69,6.71,61.24,8.2,45.5,8.84 +external/ckeditor5-commercial/packages/ckeditor5-revision-history/LICENSE.md,0,8,11.88,14.93,12.54,30.07,17.89,61.14,15.77 +external/ckeditor5-commercial/packages/ckeditor5-revision-history/README.md,0,12,9.63,12.92,10.72,39.95,14.61,53.4,13.38 +external/ckeditor5-commercial/packages/ckeditor5-revision-history/docs/api/revision-history.md,0,0,5.98,9.32,6.9,62.83,10.76,33.8,10.5 +external/ckeditor5-commercial/packages/ckeditor5-revision-history/docs/features/revision-history-integration.md,0,87,8.5,10.3,9.74,51.72,13.28,41.01,12.93 +external/ckeditor5-commercial/packages/ckeditor5-revision-history/docs/features/revision-history.md,0,5,7.32,9.97,8.99,52.66,12.25,43.97,12.04 +external/ckeditor5-commercial/packages/ckeditor5-slash-command/CHANGELOG.md,0,1,3.94,7.2,7.4,52.7,9.07,39.33,8.84 +external/ckeditor5-commercial/packages/ckeditor5-slash-command/LICENSE.md,0,7,10.63,13.23,9.92,49.36,13.31,59.29,13.26 +external/ckeditor5-commercial/packages/ckeditor5-slash-command/README.md,0,12,8.39,11.13,8.74,55.2,11.89,50.6,12.06 +external/ckeditor5-commercial/packages/ckeditor5-slash-command/docs/api/slash-command.md,0,1,3.8,7.08,5.45,69.22,8.35,32.82,9.15 +external/ckeditor5-commercial/packages/ckeditor5-slash-command/docs/features/slash-commands.md,0,31,6.6,8.8,7.84,61.97,9.15,38.14,10.34 +external/ckeditor5-commercial/packages/ckeditor5-template/CHANGELOG.md,0,1,4.28,7.69,8.73,44.05,11.83,44.96,10.13 +external/ckeditor5-commercial/packages/ckeditor5-template/LICENSE.md,0,7,10.6,13.4,10.01,47.99,13.38,60.25,13.26 +external/ckeditor5-commercial/packages/ckeditor5-template/README.md,0,12,8.58,11.59,8.79,53.88,11.43,52.1,11.75 +external/ckeditor5-commercial/packages/ckeditor5-template/docs/api/template.md,0,0,5.84,9.62,6.86,59.12,7.75,38.79,9.44 +external/ckeditor5-commercial/packages/ckeditor5-template/docs/features/template.md,0,10,7.07,9.84,8.22,57.41,10.12,41.26,10.95 +external/ckeditor5-commercial/packages/ckeditor5-track-changes/CHANGELOG.md,0,30,9.99,13.79,10.1,42.63,10.56,46.7,12.14 +external/ckeditor5-commercial/packages/ckeditor5-track-changes/LICENSE.md,0,8,11.7,14.39,11.58,38.13,15.56,59.36,14.87 +external/ckeditor5-commercial/packages/ckeditor5-track-changes/README.md,0,10,9.13,12.63,9.75,45.5,11.58,49.56,12.09 +external/ckeditor5-commercial/packages/ckeditor5-track-changes/docs/api/track-changes.md,0,0,3.28,5.15,4.51,69.83,5.89,32.57,7.45 +external/ckeditor5-commercial/packages/ckeditor5-track-changes/docs/features/track-changes-custom-features.md,0,38,9.39,11.68,9.67,51.22,11.59,46.35,12.22 +external/ckeditor5-commercial/packages/ckeditor5-track-changes/docs/features/track-changes-data.md,0,11,8.51,10.51,9.64,51.66,11.18,39.02,11.89 +external/ckeditor5-commercial/packages/ckeditor5-track-changes/docs/features/track-changes-integration.md,0,65,8.18,9.81,9.23,55.71,11.5,37.11,11.71 +external/ckeditor5-commercial/packages/ckeditor5-track-changes/docs/features/track-changes.md,0,40,7.52,9.9,8.65,56.43,10.78,38.41,11.6 +packages/ckeditor5-adapter-ckfinder/CHANGELOG.md,0,13,9.74,13.66,10.06,30.8,11.73,57.88,10.29 +packages/ckeditor5-adapter-ckfinder/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-adapter-ckfinder/LICENSE.md,0,3,10.59,13.52,10.32,45.23,14.69,57.78,13.71 +packages/ckeditor5-adapter-ckfinder/README.md,0,1,7.36,10.97,9.02,47.96,13.06,38.99,11.93 +packages/ckeditor5-adapter-ckfinder/docs/api/adapter-ckfinder.md,0,0,4.07,7.41,6.63,58.9,10.69,32.96,9.89 +packages/ckeditor5-alignment/CHANGELOG.md,0,11,9.49,13.58,9,38.94,12.64,56.23,10.49 +packages/ckeditor5-alignment/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-alignment/LICENSE.md,0,3,10.65,13.52,10.29,45.75,14.65,57.46,13.71 +packages/ckeditor5-alignment/README.md,0,1,5.6,9.34,7.06,57.12,11.27,38.7,10.13 +packages/ckeditor5-alignment/docs/api/alignment.md,0,0,3.35,5.11,5.3,64.05,8.74,32.11,7.91 +packages/ckeditor5-alignment/docs/features/text-alignment.md,0,15,6.34,9.44,7.45,60.59,9.99,37.41,10.63 +packages/ckeditor5-autoformat/CHANGELOG.md,0,27,8.16,12.54,8.8,44.11,9.89,46.87,10.22 +packages/ckeditor5-autoformat/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-autoformat/LICENSE.md,0,3,10.69,13.65,10.45,44.34,14.69,57.78,13.71 +packages/ckeditor5-autoformat/README.md,0,1,6.04,9.9,7.83,51.57,10.97,38.36,9.99 +packages/ckeditor5-autoformat/docs/api/autoformat.md,0,0,3.77,6.24,6.14,59.01,8.31,30.99,8 +packages/ckeditor5-autoformat/docs/features/autoformat.md,0,10,6.53,9.93,7.7,57.57,8.78,37.25,10.6 +packages/ckeditor5-autosave/CHANGELOG.md,0,7,9.39,13.38,8.85,39.87,10.5,54.91,10.09 +packages/ckeditor5-autosave/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-autosave/LICENSE.md,0,4,11.89,14.94,12.07,33.5,16.71,59.24,15.17 +packages/ckeditor5-autosave/README.md,0,1,5.68,9.44,7.62,52.42,12.05,40.76,10.29 +packages/ckeditor5-autosave/docs/api/autosave.md,0,0,4.06,5.99,6.49,55.44,9.56,35.32,8.17 +packages/ckeditor5-autosave/docs/features/autosave.md,0,15,6.97,8.3,8.27,62.6,9.68,35.7,10.64 +packages/ckeditor5-basic-styles/CHANGELOG.md,0,17,8.47,12.02,7.73,47.4,9.64,51.08,9.59 +packages/ckeditor5-basic-styles/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-basic-styles/LICENSE.md,0,3,10.55,13.4,10.16,46.63,14.24,56.42,13.48 +packages/ckeditor5-basic-styles/README.md,0,1,5.96,9.55,7.55,56.55,10.36,37.32,10.69 +packages/ckeditor5-basic-styles/docs/api/basic-styles.md,0,0,3.84,4.3,5.01,64.69,6.96,33.47,7.59 +packages/ckeditor5-basic-styles/docs/features/basic-styles.md,0,18,7.05,9.45,7.83,61.73,9.44,35.9,11.03 +packages/ckeditor5-block-quote/CHANGELOG.md,0,16,8.67,12.71,8.12,45.63,10.18,53.66,9.87 +packages/ckeditor5-block-quote/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-block-quote/LICENSE.md,0,3,10.5,13.34,10.04,47.51,14.24,56.42,13.48 +packages/ckeditor5-block-quote/README.md,0,1,5.1,8.72,6.23,63.06,9.87,35.2,9.52 +packages/ckeditor5-block-quote/docs/api/block-quote.md,0,0,3.15,4.73,4.83,67.17,7.84,30.12,7.55 +packages/ckeditor5-block-quote/docs/features/block-quote.md,0,16,7.38,9.91,8.47,56.96,10.63,37.56,11.47 +packages/ckeditor5-build-balloon-block/CHANGELOG.md,2,27,-5,-3.88,1.6,95.91,4.43,17.96,6.87 +packages/ckeditor5-build-balloon-block/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-build-balloon-block/LICENSE.md,0,4,11.88,14.83,11.99,34.44,16.63,57.97,15.17 +packages/ckeditor5-build-balloon-block/README.md,0,2,5.87,9.46,6.95,60.64,10.63,35.24,10.36 +packages/ckeditor5-build-balloon/CHANGELOG.md,0,59,-4.62,-3.44,2.24,92.31,4.67,18.44,7.01 +packages/ckeditor5-build-balloon/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-build-balloon/LICENSE.md,0,4,11.85,14.84,12.03,33.97,16.67,58.21,15.17 +packages/ckeditor5-build-balloon/README.md,0,2,5.62,9.24,7.05,59.13,11.23,35.13,10.43 +packages/ckeditor5-build-classic/CHANGELOG.md,0,61,-4.37,-3.13,2.37,91.47,4.72,19,7.06 +packages/ckeditor5-build-classic/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-build-classic/LICENSE.md,0,4,11.85,14.84,12.03,33.97,16.67,58.21,15.17 +packages/ckeditor5-build-classic/README.md,0,2,5.62,9.24,7.05,59.13,11.23,35.13,10.43 +packages/ckeditor5-build-decoupled-document/CHANGELOG.md,0,66,-4.54,-3.34,2.11,93.06,4.63,18.54,6.99 +packages/ckeditor5-build-decoupled-document/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-build-decoupled-document/LICENSE.md,0,4,11.88,14.88,12.12,33.3,16.98,58.21,15.32 +packages/ckeditor5-build-decoupled-document/README.md,0,2,6.41,10.15,7.95,53.32,12.27,36.8,11.31 +packages/ckeditor5-build-inline/CHANGELOG.md,0,57,-4.63,-3.46,2.25,92.27,4.65,18.42,7 +packages/ckeditor5-build-inline/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-build-inline/LICENSE.md,0,4,11.81,14.79,12.03,33.97,16.67,57.42,15.17 +packages/ckeditor5-build-inline/README.md,0,2,5.53,9.13,7.05,59.13,11.23,33.21,10.43 +packages/ckeditor5-build-multi-root/CHANGELOG.md,0,1,3.94,7.2,7.4,52.7,9.07,39.33,8.84 +packages/ckeditor5-build-multi-root/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-build-multi-root/LICENSE.md,0,4,11.77,14.69,11.99,34.44,16.63,57.19,15.17 +packages/ckeditor5-build-multi-root/README.md,0,2,5.57,9.01,7.07,60.28,11.21,33.28,10.59 +packages/ckeditor5-ckbox/CHANGELOG.md,0,1,3.94,7.2,7.4,52.7,9.07,39.33,8.84 +packages/ckeditor5-ckbox/LICENSE.md,0,4,10.23,13.48,10.26,44.07,14.19,56.53,13.73 +packages/ckeditor5-ckbox/README.md,0,4,7.12,10.21,8.76,52.21,12.01,33,11.86 +packages/ckeditor5-ckbox/docs/api/ckbox.md,0,3,3.64,6.89,6.14,64.06,9.66,27.31,9.85 +packages/ckeditor5-ckbox/docs/features/ckbox.md,0,46,8.15,10.19,9.26,53.96,10.36,38.53,12.04 +packages/ckeditor5-ckfinder/CHANGELOG.md,0,6,10.75,14.58,10.24,28.96,11.41,62.11,10.05 +packages/ckeditor5-ckfinder/LICENSE.md,0,3,10.59,13.52,10.2,46.12,14.27,57.78,13.48 +packages/ckeditor5-ckfinder/README.md,0,4,7.79,10.99,9.19,49.36,11.98,36.82,11.86 +packages/ckeditor5-ckfinder/docs/api/ckfinder.md,0,3,3.33,6.52,5.86,65.39,8.99,26.92,9.47 +packages/ckeditor5-ckfinder/docs/features/ckfinder.md,0,26,6.91,9.68,8.68,53.91,10.26,37.17,11.49 +packages/ckeditor5-clipboard/CHANGELOG.md,0,34,8.33,12.75,9.73,37.92,11.55,46.28,11.04 +packages/ckeditor5-clipboard/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-clipboard/LICENSE.md,0,4,11.92,14.99,11.97,34.17,16.39,59.24,15.02 +packages/ckeditor5-clipboard/README.md,0,1,5.69,9.46,7.26,55.77,10.86,39.14,9.96 +packages/ckeditor5-clipboard/docs/api/clipboard.md,0,1,5.26,8.73,7.45,51.59,7.95,37.74,9.12 +packages/ckeditor5-clipboard/docs/features/drag-drop.md,0,19,6.64,8.85,7.21,66.55,8.64,29.97,9.85 +packages/ckeditor5-clipboard/docs/features/paste-plain-text.md,0,12,7.59,10.53,7.5,62.42,7.89,37.99,9.81 +packages/ckeditor5-clipboard/docs/framework/deep-dive/clipboard.md,0,22,6.61,8.7,7.58,64.32,9.2,38.2,10.13 +packages/ckeditor5-cloud-services/CHANGELOG.md,0,21,9.97,14.79,11.64,24.45,13.1,54.84,11.99 +packages/ckeditor5-cloud-services/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-cloud-services/LICENSE.md,0,3,10.84,13.77,10.53,43.98,14.65,57.46,13.93 +packages/ckeditor5-cloud-services/README.md,0,1,6.14,10.02,7.98,50.06,11.08,38.11,10.29 +packages/ckeditor5-cloud-services/docs/api/cloud-services.md,0,0,4.15,5.9,6.33,56.39,8.17,33.75,8.08 +packages/ckeditor5-code-block/CHANGELOG.md,0,3,7.81,11.62,7.55,49.76,10.08,50.51,9.67 +packages/ckeditor5-code-block/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-code-block/LICENSE.md,0,4,11.77,14.74,11.84,35.3,16.35,58.21,15.02 +packages/ckeditor5-code-block/README.md,0,1,4.83,8.22,5.82,68.28,9.32,33.75,9.52 +packages/ckeditor5-code-block/docs/api/code-block.md,0,0,2.86,4.5,4.69,68.38,7.71,29.54,7.55 +packages/ckeditor5-code-block/docs/features/code-blocks.md,1,36,8.13,9.64,8.33,62.51,10.36,38.33,11.59 +packages/ckeditor5-core/CHANGELOG.md,0,40,6.53,10.38,6.86,56.23,8.35,41.66,9.22 +packages/ckeditor5-core/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-core/LICENSE.md,0,4,11.99,15.02,12.21,32.64,16.98,58.21,15.32 +packages/ckeditor5-core/README.md,0,2,8.79,12.76,10.37,38.32,16.1,44.05,13.02 +packages/ckeditor5-core/docs/api/core.md,0,0,4.85,8.41,6.65,59.39,10.26,32.4,9.57 +packages/ckeditor5-easy-image/CHANGELOG.md,0,24,10.45,15.36,12.49,19.12,13.39,55.19,12.29 +packages/ckeditor5-easy-image/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-easy-image/LICENSE.md,0,3,10.45,13.28,10.29,45.75,14.24,56.42,13.48 +packages/ckeditor5-easy-image/README.md,0,3,8.01,11.66,9.72,43.68,10.83,35.94,11.74 +packages/ckeditor5-easy-image/docs/api/easy-image.md,0,2,5.55,9.29,7.87,50.63,9.12,33.91,10.32 +packages/ckeditor5-easy-image/docs/features/easy-image.md,0,37,7.83,9.96,9.09,54.52,9.99,37.66,12.12 +packages/ckeditor5-editor-balloon/CHANGELOG.md,0,30,8.8,13.34,10.02,35.47,12.42,49.33,11.24 +packages/ckeditor5-editor-balloon/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-editor-balloon/LICENSE.md,0,4,12.15,15.33,12.29,31.66,16.76,58.69,15.17 +packages/ckeditor5-editor-balloon/README.md,0,2,6.76,10.33,8.81,48.89,12.58,36.21,11.57 +packages/ckeditor5-editor-balloon/docs/api/editor-balloon.md,0,1,4.5,7.84,6.8,56.6,10.31,29.77,9.27 +packages/ckeditor5-editor-classic/CHANGELOG.md,0,27,8.25,12.62,9.22,42.52,11.52,46.63,11.18 +packages/ckeditor5-editor-classic/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-editor-classic/LICENSE.md,0,4,12.15,15.33,12.29,31.66,16.76,58.69,15.17 +packages/ckeditor5-editor-classic/README.md,0,2,6.57,10.18,8.5,50.62,11.94,36.72,11.21 +packages/ckeditor5-editor-classic/docs/api/editor-classic.md,0,1,4.3,7.52,6.4,59.04,9.44,30.54,8.84 +packages/ckeditor5-editor-decoupled/CHANGELOG.md,0,24,9.12,13.73,10.4,33.36,12.26,49.55,11.31 +packages/ckeditor5-editor-decoupled/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-editor-decoupled/LICENSE.md,0,4,12.23,15.42,12.39,30.98,16.76,58.69,15.32 +packages/ckeditor5-editor-decoupled/README.md,0,2,6.8,10.47,8.85,48.13,12.34,36.72,11.74 +packages/ckeditor5-editor-decoupled/docs/api/editor-decoupled.md,0,1,4.69,8.01,6.89,55.51,9.44,30.54,9.47 +packages/ckeditor5-editor-decoupled/docs/framework/document-editor.md,0,15,7.89,9.48,8.87,58.12,11.94,37.11,12.39 +packages/ckeditor5-editor-inline/CHANGELOG.md,0,34,8.19,12.55,9.05,43.54,11.17,46.23,10.93 +packages/ckeditor5-editor-inline/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-editor-inline/LICENSE.md,0,4,12.12,15.28,12.29,31.66,16.76,57.89,15.17 +packages/ckeditor5-editor-inline/README.md,0,2,6.39,9.95,8.62,49.79,11.94,33.78,11.39 +packages/ckeditor5-editor-inline/docs/api/editor-inline.md,0,1,4.1,7.28,6.4,59.04,9.44,26.37,8.84 +packages/ckeditor5-editor-multi-root/CHANGELOG.md,0,1,3.94,7.2,7.4,52.7,9.07,39.33,8.84 +packages/ckeditor5-editor-multi-root/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-editor-multi-root/LICENSE.md,0,4,12.07,15.17,12.25,32.15,16.71,57.65,15.17 +packages/ckeditor5-editor-multi-root/README.md,0,2,5.58,9.15,7.44,56.73,10.89,31.19,10.5 +packages/ckeditor5-editor-multi-root/docs/api/editor-multi-root.md,0,1,3.04,5.66,5.14,67.01,8.22,25.24,8.14 +packages/ckeditor5-engine/CHANGELOG.md,1,262,6.59,10.58,6.82,59.09,7.89,38.18,9.32 +packages/ckeditor5-engine/CONTRIBUTING.md,0,0,6.76,9.84,8.01,57.23,15.31,38.27,13.02 +packages/ckeditor5-engine/LICENSE.md,0,4,11.81,14.85,12.07,33.5,16.39,58.44,15.17 +packages/ckeditor5-engine/README.md,0,20,8.35,11.83,9.53,46.29,11.78,41.76,12.19 +packages/ckeditor5-engine/docs/api/engine.md,0,0,4.78,8.33,6.81,58.24,9.18,33.75,9.57 +packages/ckeditor5-engine/docs/framework/deep-dive/conversion/downcast.md,0,28,6.75,8.74,8.01,61.73,9.79,38.34,11.24 +packages/ckeditor5-engine/docs/framework/deep-dive/conversion/helpers/downcast.md,0,43,8.64,10.22,9.39,55.14,11.04,42,12.02 +packages/ckeditor5-engine/docs/framework/deep-dive/conversion/helpers/intro.md,0,3,5.38,8,7.03,64.73,9.58,32.8,10.91 +packages/ckeditor5-engine/docs/framework/deep-dive/conversion/helpers/upcast.md,0,45,9.53,10.64,10.47,49.76,12.33,45.13,13.2 +packages/ckeditor5-engine/docs/framework/deep-dive/conversion/intro.md,0,15,6.77,9.4,7.75,61.09,9.25,34.12,10.41 +packages/ckeditor5-engine/docs/framework/deep-dive/conversion/upcast.md,0,21,7.95,9.77,9.07,55.96,10.01,40.24,11.63 +packages/ckeditor5-engine/docs/framework/deep-dive/event-system.md,0,46,4.42,7.85,6.23,63.8,8.24,33.23,9.5 +packages/ckeditor5-engine/docs/framework/deep-dive/schema.md,0,106,9.05,10.54,9.55,54.66,11.01,41.56,11.87 +packages/ckeditor5-enter/CHANGELOG.md,0,27,8.72,13.21,9.85,37.73,11.33,48.66,11.25 +external/ckeditor5-commercial/packages/ckeditor5-ai/CHANGELOG.md,0,1,4.35,7.69,6.71,61.24,8.2,45.5,8.84 +external/ckeditor5-commercial/packages/ckeditor5-ai/LICENSE.md,0,9,11.11,13.97,11.85,35.01,16.55,57.45,15.36 +external/ckeditor5-commercial/packages/ckeditor5-ai/README.md,0,13,9.43,12.17,10.02,47.05,13.22,53.53,13.37 +external/ckeditor5-commercial/packages/ckeditor5-ai/docs/api/ai.md,0,1,6.91,10.93,7.99,48.68,11.43,40.82,10.13 +external/ckeditor5-commercial/packages/ckeditor5-ai/docs/features/ai-assistant-integration.md,0,69,7.56,10.13,8.58,56.22,10.94,39.62,11.45 +external/ckeditor5-commercial/packages/ckeditor5-ai/docs/features/ai-assistant-overview.md,0,22,8.15,10.33,9.08,54.72,11.27,42.07,12.02 +external/ckeditor5-commercial/packages/ckeditor5-case-change/CHANGELOG.md,0,1,4.28,7.69,8.73,44.05,11.83,44.96,10.13 +external/ckeditor5-commercial/packages/ckeditor5-case-change/LICENSE.md,0,6,10.24,12.81,9.59,51.48,13.33,56.57,13.26 +external/ckeditor5-commercial/packages/ckeditor5-case-change/README.md,0,12,7.4,9.92,7.66,62.81,10.88,44.56,11.42 +external/ckeditor5-commercial/packages/ckeditor5-case-change/docs/api/case-change.md,0,0,4.41,7.54,5.28,66.55,6.86,31.44,8.08 +external/ckeditor5-commercial/packages/ckeditor5-case-change/docs/features/case-change.md,0,11,5.18,8.24,6.26,67.9,8.12,32.64,9.39 +external/ckeditor5-commercial/packages/ckeditor5-collaboration-core/CHANGELOG.md,0,13,10.14,14.81,12.57,20.25,12.18,49.53,12.2 +external/ckeditor5-commercial/packages/ckeditor5-collaboration-core/LICENSE.md,0,8,11.61,14.54,12,34.12,16.14,55.94,14.98 +external/ckeditor5-commercial/packages/ckeditor5-collaboration-core/README.md,0,12,11.22,13.88,12.08,34.26,15.37,55.48,14.27 +external/ckeditor5-commercial/packages/ckeditor5-collaboration/CHANGELOG.md,0,1,4.35,7.69,6.71,61.24,8.2,45.5,8.84 +external/ckeditor5-commercial/packages/ckeditor5-collaboration/LICENSE.md,0,8,11.62,14.74,12.15,32.37,16.28,56.77,14.98 +external/ckeditor5-commercial/packages/ckeditor5-collaboration/README.md,0,12,11.37,14.13,12.34,32.14,15.78,56.66,14.46 +external/ckeditor5-commercial/packages/ckeditor5-collaboration/docs/features/collaboration.md,0,18,9.52,11.76,10.25,47.36,12.05,43.94,12.19 +external/ckeditor5-commercial/packages/ckeditor5-comments/CHANGELOG.md,0,57,8.52,12.19,9.33,47.01,9.6,45.16,10.85 +external/ckeditor5-commercial/packages/ckeditor5-comments/LICENSE.md,0,8,11.03,13.97,11.17,39.57,15.09,57.28,14.41 +external/ckeditor5-commercial/packages/ckeditor5-comments/README.md,0,17,9.34,12.38,10.29,43.79,12.92,51.42,12.86 +external/ckeditor5-commercial/packages/ckeditor5-comments/docs/api/comments.md,0,0,4.23,4.02,5.99,57.15,5.84,38.13,6.82 +external/ckeditor5-commercial/packages/ckeditor5-comments/docs/features/annotations-custom-configuration.md,0,11,7.89,10.76,9.29,50.22,11.43,39.98,11.55 +external/ckeditor5-commercial/packages/ckeditor5-comments/docs/features/annotations-custom-template.md,0,13,5.21,8.54,5.9,68.88,7.71,34.46,9.19 +external/ckeditor5-commercial/packages/ckeditor5-comments/docs/features/annotations-custom-theme.md,0,3,9.37,10.15,10.9,47.71,14.07,42.01,13.61 +external/ckeditor5-commercial/packages/ckeditor5-comments/docs/features/annotations-custom-view.md,1,36,7.17,8.88,7.26,68.59,9.04,35.13,9.95 +external/ckeditor5-commercial/packages/ckeditor5-comments/docs/features/annotations-display-mode.md,0,42,9.61,11.1,9.82,53.17,11.86,44.17,12.18 +external/ckeditor5-commercial/packages/ckeditor5-comments/docs/features/annotations.md,0,5,7.23,11.26,7.84,53.37,10.49,36.54,10.19 +external/ckeditor5-commercial/packages/ckeditor5-comments/docs/features/comments-archive.md,0,17,8.88,10.09,8.64,62,9.92,43.03,10.18 +external/ckeditor5-commercial/packages/ckeditor5-comments/docs/features/comments-integration.md,0,63,8.01,9.64,8.99,57.25,10.91,38.22,11.34 +external/ckeditor5-commercial/packages/ckeditor5-comments/docs/features/comments-only-mode.md,0,12,8.39,9.46,8.96,59.75,10.81,36.76,11.37 +external/ckeditor5-commercial/packages/ckeditor5-comments/docs/features/comments-outside-editor.md,0,24,6.23,8.66,6.89,67.49,8.88,34.6,10.13 +external/ckeditor5-commercial/packages/ckeditor5-comments/docs/features/comments-walkthrough.md,0,37,8.34,11.17,8.25,58.3,9.19,44.21,10.77 +external/ckeditor5-commercial/packages/ckeditor5-comments/docs/features/comments.md,0,49,7.6,9.69,8.26,60.35,9.71,41.1,10.99 +external/ckeditor5-commercial/packages/ckeditor5-document-outline/CHANGELOG.md,0,1,4.28,7.69,8.73,44.05,11.83,44.96,10.13 +external/ckeditor5-commercial/packages/ckeditor5-document-outline/LICENSE.md,0,8,12.45,15.15,12.46,32.44,16.67,63.89,15.47 +external/ckeditor5-commercial/packages/ckeditor5-document-outline/README.md,0,12,9.12,11.87,9.45,50.82,12.5,54.35,12.37 +external/ckeditor5-commercial/packages/ckeditor5-document-outline/docs/api/document-outline.md,0,0,5.57,9.25,7.47,55.42,9.21,39.81,9.52 +external/ckeditor5-commercial/packages/ckeditor5-document-outline/docs/features/document-outline.md,0,15,6.57,9.83,8.04,55.91,9.98,38.86,10.75 +external/ckeditor5-commercial/packages/ckeditor5-document-outline/docs/features/table-of-contents.md,0,8,6.66,9.43,7.87,59.49,10.12,40.18,10.75 +external/ckeditor5-commercial/packages/ckeditor5-export-pdf/CHANGELOG.md,0,1,4.28,7.69,8.73,44.05,11.83,44.96,10.13 +external/ckeditor5-commercial/packages/ckeditor5-export-pdf/LICENSE.md,0,8,9.98,12.22,9.85,50.63,13.26,55.49,13.26 +external/ckeditor5-commercial/packages/ckeditor5-export-pdf/README.md,0,14,7.6,10.04,8.54,56.98,11.62,46.11,11.96 +external/ckeditor5-commercial/packages/ckeditor5-export-pdf/docs/api/export-pdf.md,0,1,1.94,4.46,4.44,72.59,7.81,26.68,8.08 +external/ckeditor5-commercial/packages/ckeditor5-export-pdf/docs/features/export-pdf.md,0,50,5.84,7.95,7.01,67.55,9.64,33.21,10.51 +external/ckeditor5-commercial/packages/ckeditor5-export-word/CHANGELOG.md,0,1,4.28,7.69,8.73,44.05,11.83,44.96,10.13 +external/ckeditor5-commercial/packages/ckeditor5-export-word/LICENSE.md,0,7,10.12,12.39,9.85,50.63,13.26,55.49,13.26 +external/ckeditor5-commercial/packages/ckeditor5-export-word/README.md,0,14,7.8,10.26,8.53,57.24,11.48,45.98,11.96 +external/ckeditor5-commercial/packages/ckeditor5-export-word/docs/api/export-word.md,0,0,2.16,4.67,4.54,71.64,7.9,27.08,8.08 +external/ckeditor5-commercial/packages/ckeditor5-export-word/docs/features/export-word.md,0,37,6.65,9.01,7.74,62.1,9.99,36.02,11.17 +external/ckeditor5-commercial/packages/ckeditor5-format-painter/CHANGELOG.md,0,1,3.94,7.2,7.4,52.7,9.07,39.33,8.84 +external/ckeditor5-commercial/packages/ckeditor5-format-painter/LICENSE.md,0,7,10.77,13.41,10.28,46.82,13.31,59.29,13.26 +external/ckeditor5-commercial/packages/ckeditor5-format-painter/README.md,0,12,8.51,11.09,9.1,53.36,11.68,48.94,12.06 +external/ckeditor5-commercial/packages/ckeditor5-format-painter/docs/api/format-painter.md,0,0,3.86,7.18,5.82,66.02,7.76,29.93,9.19 +external/ckeditor5-commercial/packages/ckeditor5-format-painter/docs/features/format-painter.md,0,24,6.73,9.22,7.54,63.17,8.47,36.04,10.71 +external/ckeditor5-commercial/packages/ckeditor5-import-word/CHANGELOG.md,0,1,4.28,7.69,8.73,44.05,11.83,44.96,10.13 \ No newline at end of file