diff --git a/.eslintrc.js b/.eslintrc.js
index 880651d7f44..90499b7e2a0 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -7,16 +7,8 @@
'use strict';
-const fs = require( 'fs' );
-const path = require( 'path' );
-
-const dllPackages = fs.readdirSync( path.join( __dirname, 'src' ) ).map( directory => directory.replace( /\.js$/, '' ) );
-
module.exports = {
extends: 'ckeditor5',
- settings: {
- dllPackages
- },
rules: {
'ckeditor5-rules/ckeditor-imports': 'error'
},
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3e557f391a3..40c2fe548ab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,9 +9,9 @@ We are happy to announce the release of CKEditor 5 v29.2.0.
This release introduces several new features:
-* [Redesigned](https://github.com/ckeditor/ckeditor5/issues/10229) the find and replace panel and introduced [a few improvements to the feature itself]((https://github.com/ckeditor/ckeditor5/issues?q=is%3Aissue+milestone%3A%22iteration+46%22+-label%3Atype%3Adocs+-label%3Atype%3Atask+sort%3Aupdated-desc+label%3Apackage%3Afind-and-replace)).
-* Added the possibility to create a localized editor when using [DLL builds](https://ckeditor.com/docs/ckeditor5/latest/builds/guides/development/dll-builds.html).
-* Improved the performance when [pasting large images](https://github.com/ckeditor/ckeditor5/issues/10287).
+* [Redesigned find and replace panel](https://github.com/ckeditor/ckeditor5/issues/10229) and [a few improvements to the feature itself]((https://github.com/ckeditor/ckeditor5/issues?q=is%3Aissue+milestone%3A%22iteration+46%22+-label%3Atype%3Adocs+-label%3Atype%3Atask+sort%3Aupdated-desc+label%3Apackage%3Afind-and-replace)).
+* The possibility to create a localized editor when using [DLL builds](https://ckeditor.com/docs/ckeditor5/latest/builds/guides/development/dll-builds.html).
+* Improved performance when [pasting large images](https://github.com/ckeditor/ckeditor5/issues/10287).
There were also a few bug fixes:
@@ -19,13 +19,13 @@ There were also a few bug fixes:
* The highlight feature [can now be used while typing](https://github.com/ckeditor/ckeditor5/issues/2616).
* Pasted HTML comments [are filtered out](https://github.com/ckeditor/ckeditor5/issues/10213).
-
+Read more in the blog post: https://ckeditor.com/blog/ckeditor-5-v29.2.0-with-redesigned-find-and-replace-and-localized-dll-builds/
### MINOR BREAKING CHANGES [โน๏ธ](https://ckeditor.com/docs/ckeditor5/latest/framework/guides/support/versioning-policy.html#major-and-minor-breaking-changes)
* **[find-and-replace](https://www.npmjs.com/package/@ckeditor/ckeditor5-find-and-replace)**: The layout, styling, and view structure of the find and replace form have changed radically, which may affect integrations that either customized or extended this form (see [#10229](https://github.com/ckeditor/ckeditor5/issues/10229)).
-* **[revision-history](https://www.npmjs.com/package/@ckeditor/ckeditor5-revision-history)**: The revision data now includes a new property: `authorsIds`. This property needs to be handled (saved and loaded) similarly to other revision properties. For revisions that are already saved in your database, set this value to an array with one string, equal to the `creatorId` value (e.g. `["user1"]`). Check the updated [revision history integration](https://ckeditor.com/docs/ckeditor5/latest/features/revision-history/revision-history-integration.html) guide to see an example.
-* **[revision-history](https://www.npmjs.com/package/@ckeditor/ckeditor5-revision-history)**: The documentation for revision history adapter has been updated. Please check the `RevisionHistoryAdapter#addRevision()` and `updateRevision()` documentation to make sure that you correctly handle all the data passed to those methods.
+* **[revision-history](https://www.npmjs.com/package/@ckeditor/ckeditor5-revision-history)**: The revision data now includes a new property: `authorsIds`. This property needs to be handled (saved and loaded) similarly to other revision properties. For revisions that are already saved in your database, set this value to an array with one string, equal to the `creatorId` value (e.g. `["user1"]`). Check the updated [revision history integration guide](https://ckeditor.com/docs/ckeditor5/latest/features/revision-history/revision-history-integration.html) to see an example.
+* **[revision-history](https://www.npmjs.com/package/@ckeditor/ckeditor5-revision-history)**: The documentation for revision history adapter has been updated. Please check the `RevisionHistoryAdapter#addRevision()` and `updateRevision()` documentation to make sure that you correctly handle all the data passed to these methods.
### Features
@@ -36,18 +36,18 @@ There were also a few bug fixes:
### Bug fixes
-* **[comments](https://www.npmjs.com/package/@ckeditor/ckeditor5-comments)**: `Comment#setAttribute()` and `Comment#removeAttribute()` will now correctly set the attribute value and fire the adapter call also for comments created by other users.
+* **[comments](https://www.npmjs.com/package/@ckeditor/ckeditor5-comments)**: The `Comment#setAttribute()` and `Comment#removeAttribute()` methods will now correctly set the attribute value and fire the adapter call also for comments created by other users.
* **[find-and-replace](https://www.npmjs.com/package/@ckeditor/ckeditor5-find-and-replace)**: Changing the search text should reset the results. Closes [#10304](https://github.com/ckeditor/ckeditor5/issues/10304). ([commit](https://github.com/ckeditor/ckeditor5/commit/dc3160944d6d0c95469e982a47936d47aa1bbd64))
* **[find-and-replace](https://www.npmjs.com/package/@ckeditor/ckeditor5-find-and-replace)**: Toggling search options should reset the results. Closes [#10021](https://github.com/ckeditor/ckeditor5/issues/10021). ([commit](https://github.com/ckeditor/ckeditor5/commit/dc3160944d6d0c95469e982a47936d47aa1bbd64))
* **[find-and-replace](https://www.npmjs.com/package/@ckeditor/ckeditor5-find-and-replace)**: The find and replace form should be responsive. Closes [#10019](https://github.com/ckeditor/ckeditor5/issues/10019). ([commit](https://github.com/ckeditor/ckeditor5/commit/dc3160944d6d0c95469e982a47936d47aa1bbd64))
-* **[find-and-replace](https://www.npmjs.com/package/@ckeditor/ckeditor5-find-and-replace)**: Allows search term to contain a trailing/leading space when searching "whole words only". Closes [#10131](https://github.com/ckeditor/ckeditor5/issues/10131). ([commit](https://github.com/ckeditor/ckeditor5/commit/fd9dfa7658e7ee3e968612274294baf224828326))
-* **[html-support](https://www.npmjs.com/package/@ckeditor/ckeditor5-html-support)**: Filters out all pasted HTML comments. Closes [#10213](https://github.com/ckeditor/ckeditor5/issues/10213). ([commit](https://github.com/ckeditor/ckeditor5/commit/59e23271a9ed32fb7bdc4eb51360b5e0f5d7b0ba))
+* **[find-and-replace](https://www.npmjs.com/package/@ckeditor/ckeditor5-find-and-replace)**: The search term should be allowed to contain a trailing or leading space when searching "whole words only". Closes [#10131](https://github.com/ckeditor/ckeditor5/issues/10131). ([commit](https://github.com/ckeditor/ckeditor5/commit/fd9dfa7658e7ee3e968612274294baf224828326))
+* **[html-support](https://www.npmjs.com/package/@ckeditor/ckeditor5-html-support)**: All pasted HTML comments will now be filtered. Closes [#10213](https://github.com/ckeditor/ckeditor5/issues/10213). ([commit](https://github.com/ckeditor/ckeditor5/commit/59e23271a9ed32fb7bdc4eb51360b5e0f5d7b0ba))
* **[html-support](https://www.npmjs.com/package/@ckeditor/ckeditor5-html-support)**: Extended the schema definition for `$root` to allow storing a comment's content as the `$root` attribute. Closes [#10274](https://github.com/ckeditor/ckeditor5/issues/10274). ([commit](https://github.com/ckeditor/ckeditor5/commit/35b08a96f41423b1e1173af60c186fb0193c92cb))
* **[revision-history](https://www.npmjs.com/package/@ckeditor/ckeditor5-revision-history)**: Enabled multiple authors in one revision. Introduced the `authorsIds` property in the revision data.
-* **[revision-history](https://www.npmjs.com/package/@ckeditor/ckeditor5-revision-history)**: Visual improvements on how nested changes are displayed.
-* **[source-editing](https://www.npmjs.com/package/@ckeditor/ckeditor5-source-editing)**: The selection is now set at the beginning of the source editing view. Closes [#10180](https://github.com/ckeditor/ckeditor5/issues/10180). ([commit](https://github.com/ckeditor/ckeditor5/commit/1e5b03d8a91038dfcf4a7afc7a47f11cb7a27041))
+* **[revision-history](https://www.npmjs.com/package/@ckeditor/ckeditor5-revision-history)**: Visual improvements to how nested changes are displayed.
+* **[source-editing](https://www.npmjs.com/package/@ckeditor/ckeditor5-source-editing)**: The selection is now set to the beginning of the source editing view. Closes [#10180](https://github.com/ckeditor/ckeditor5/issues/10180). ([commit](https://github.com/ckeditor/ckeditor5/commit/1e5b03d8a91038dfcf4a7afc7a47f11cb7a27041))
* **[source-editing](https://www.npmjs.com/package/@ckeditor/ckeditor5-source-editing)**: The source editing feature will send a warning to the console when the restricted editing feature is loaded. Closes [#10228](https://github.com/ckeditor/ckeditor5/issues/10228). ([commit](https://github.com/ckeditor/ckeditor5/commit/42a31c423c1b770e7a25d3dbb546fc1327100779))
-* **[theme-lark](https://www.npmjs.com/package/@ckeditor/ckeditor5-theme-lark)**: The label of the labeled field should stay on the top when the field is disabled and not empty to not cover the field's text (see [#10229](https://github.com/ckeditor/ckeditor5/issues/10229)). ([commit](https://github.com/ckeditor/ckeditor5/commit/dc3160944d6d0c95469e982a47936d47aa1bbd64))
+* **[theme-lark](https://www.npmjs.com/package/@ckeditor/ckeditor5-theme-lark)**: The label of the labeled field should stay at the top when the field is disabled and not empty to not cover the field's text (see [#10229](https://github.com/ckeditor/ckeditor5/issues/10229)). ([commit](https://github.com/ckeditor/ckeditor5/commit/dc3160944d6d0c95469e982a47936d47aa1bbd64))
### Other changes
@@ -55,14 +55,14 @@ There were also a few bug fixes:
* **[clipboard](https://www.npmjs.com/package/@ckeditor/ckeditor5-clipboard)**: The `DataTransfer.files` property is not evaluated more than once. Closes [#10287](https://github.com/ckeditor/ckeditor5/issues/10287). ([commit](https://github.com/ckeditor/ckeditor5/commit/2bde3d069ead725d8fc2f6e7379da05523c29fde))
* **[comments](https://www.npmjs.com/package/@ckeditor/ckeditor5-comments)**: Raised comments character limit to 65000.
* **[core](https://www.npmjs.com/package/@ckeditor/ckeditor5-core)**: Merged a duplicated translation context from `ckeditor5-ui` and `ckeditor5-find-and-replace` packages. Closes [#10400](https://github.com/ckeditor/ckeditor5/issues/10400). ([commit](https://github.com/ckeditor/ckeditor5/commit/f931085112d9f8cdeb731543a01729058c7e3ce6))
-* **[find-and-replace](https://www.npmjs.com/package/@ckeditor/ckeditor5-find-and-replace)**: Moved the search result translation context to ckeditor5-core. Closes [#10400](https://github.com/ckeditor/ckeditor5/issues/10400). ([commit](https://github.com/ckeditor/ckeditor5/commit/f931085112d9f8cdeb731543a01729058c7e3ce6))
+* **[find-and-replace](https://www.npmjs.com/package/@ckeditor/ckeditor5-find-and-replace)**: Moved the search result translation context to `ckeditor5-core`. Closes [#10400](https://github.com/ckeditor/ckeditor5/issues/10400). ([commit](https://github.com/ckeditor/ckeditor5/commit/f931085112d9f8cdeb731543a01729058c7e3ce6))
* **[find-and-replace](https://www.npmjs.com/package/@ckeditor/ckeditor5-find-and-replace)**: Visually revamped the find and replace form. Closes [#10229](https://github.com/ckeditor/ckeditor5/issues/10229). ([commit](https://github.com/ckeditor/ckeditor5/commit/dc3160944d6d0c95469e982a47936d47aa1bbd64))
* **[find-and-replace](https://www.npmjs.com/package/@ckeditor/ckeditor5-find-and-replace)**: Increased the contrast between selected and unselected find and replace results. Closes [#10242](https://github.com/ckeditor/ckeditor5/issues/10242). ([commit](https://github.com/ckeditor/ckeditor5/commit/f5a2c57aa7b2aeea5a01ad1e7c7bf0a9b9118078))
* **[highlight](https://www.npmjs.com/package/@ckeditor/ckeditor5-highlight)**: Toggling highlight does not remove it when the caret is at the end of the highlighted range. Closes [#2616](https://github.com/ckeditor/ckeditor5/issues/2616). ([commit](https://github.com/ckeditor/ckeditor5/commit/d1a271d127777bb0f33f0e4e52222f0fbf21f6c2))
* **[language](https://www.npmjs.com/package/@ckeditor/ckeditor5-language)**: The "Remove language" option of text part language dropdown is now the first one in the list. Closes [#10338](https://github.com/ckeditor/ckeditor5/issues/10338). ([commit](https://github.com/ckeditor/ckeditor5/commit/e6cd6e487d6ca528d19734ac27b447003063ca5c))
* **[revision-history](https://www.npmjs.com/package/@ckeditor/ckeditor5-revision-history)**: Some CSS styling improvements for suggestions and changes highlights.
* **[theme-lark](https://www.npmjs.com/package/@ckeditor/ckeditor5-theme-lark)**: Moved the presentational find and replace form styles to the theme (see [#10229](https://github.com/ckeditor/ckeditor5/issues/10229)). ([commit](https://github.com/ckeditor/ckeditor5/commit/dc3160944d6d0c95469e982a47936d47aa1bbd64))
-* **[ui](https://www.npmjs.com/package/@ckeditor/ckeditor5-ui)**: Moved the page label translation context to ckeditor5-core. Closes [#10400](https://github.com/ckeditor/ckeditor5/issues/10400). ([commit](https://github.com/ckeditor/ckeditor5/commit/f931085112d9f8cdeb731543a01729058c7e3ce6))
+* **[ui](https://www.npmjs.com/package/@ckeditor/ckeditor5-ui)**: Moved the page label translation context to `ckeditor5-core`. Closes [#10400](https://github.com/ckeditor/ckeditor5/issues/10400). ([commit](https://github.com/ckeditor/ckeditor5/commit/f931085112d9f8cdeb731543a01729058c7e3ce6))
* Updated translations. ([commit](https://github.com/ckeditor/ckeditor5/commit/d88cc7e93d4fbb0ad4f68943cff55d37532ec2cb), [commit](https://github.com/ckeditor/ckeditor5/commit/70bd66b567f19450717ed69ce999521e5c4aa26e))
* The content styles stylesheet for the guide will now be generated on-demand using the `{@exec...}` feature. Closes [#10299](https://github.com/ckeditor/ckeditor5/issues/10299). ([commit](https://github.com/ckeditor/ckeditor5/commit/18123a72726a2f9f052189b89ec58218603d26da))
diff --git a/docs/assets/img/cog.svg b/docs/assets/img/cog.svg
new file mode 100644
index 00000000000..588fb8478ba
--- /dev/null
+++ b/docs/assets/img/cog.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/assets/img/document.svg b/docs/assets/img/document.svg
new file mode 100644
index 00000000000..0546ab556e7
--- /dev/null
+++ b/docs/assets/img/document.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/assets/styles.css b/docs/assets/styles.css
index f0561fb9eea..0adeeae4c7c 100644
--- a/docs/assets/styles.css
+++ b/docs/assets/styles.css
@@ -51,3 +51,17 @@ https://github.com/ckeditor/ckeditor5-build-decoupled-document/issues/12 */
display: inline;
}
}
+
+/* https://github.com/ckeditor/ckeditor5/issues/9700 */
+.main .main__content-inner .features-html-output .output-overview-table-icon {
+ display: inline;
+ margin: 0;
+ height: 1rem;
+ vertical-align: middle;
+ position: relative;
+ top: -1px;
+}
+
+.main .main__content-inner .features-html-output th {
+ background-color: hsl(0, 0%, 96%);
+}
diff --git a/docs/builds/guides/development/dll-builds.md b/docs/builds/guides/development/dll-builds.md
index e9ae54cad9a..0205f6dd5f2 100644
--- a/docs/builds/guides/development/dll-builds.md
+++ b/docs/builds/guides/development/dll-builds.md
@@ -22,7 +22,7 @@ DLL builds are based on the [DLL webpack](https://webpack.js.org/plugins/dll-plu
Currently, CKEditor 5 does not come with a ready-to-use DLL build. Using this integration method requires creating it on your own, based on the tools available in the {@link framework/guides/contributing/development-environment CKEditor 5 development environment}.
-Follow the [Ship CKEditor 5 DLLs](https://github.com/ckeditor/ckeditor5/issues/9145) issue for updates (and add ๐ if you are interested in this functionality).
+Follow the [Ship CKEditor 5 DLLs](https://github.com/ckeditor/ckeditor5/issues/9145) issue for updates (and add ๐ if you are interested in this functionality).
## Anatomy of a DLL build
@@ -151,6 +151,12 @@ For example:
```
+## Live implementation sample
+
+Presented below is a working sample editor using the DLL mechanism. Observe the source and then click **"Result"** to switch to the live view of the working CKEditor 5 instance.
+
+
+
## Localization
All DLL builds use the default (English) translation files. However, a localized version of the the editor can be easily configured.
diff --git a/docs/builds/guides/migration/migration-to-25.md b/docs/builds/guides/migration/migration-to-25.md
index b8ebd038c9c..55079264176 100644
--- a/docs/builds/guides/migration/migration-to-25.md
+++ b/docs/builds/guides/migration/migration-to-25.md
@@ -4,6 +4,12 @@ menu-title: Migration to v25.x
order: 99
---
+
+ When updating your CKEditor 5 installation, make sure **all the packages are the same version** to avoid errors.
+
+ For custom builds, you may try removing the `package-lock.json` or `yarn.lock` files (if applicable) and reinstalling all packages before rebuilding the editor. For best results, make sure you use the most recent package versions.
+
+
# Migration to CKEditor 5 v25.0.0
For the entire list of changes introduced in version 25.0.0, see the [changelog for CKEditor 5 v25.0.0](https://github.com/ckeditor/ckeditor5/blob/master/CHANGELOG.md#2500-2021-01-25).
diff --git a/docs/builds/guides/migration/migration-to-26.md b/docs/builds/guides/migration/migration-to-26.md
index b4dd375de03..917b35c82d3 100644
--- a/docs/builds/guides/migration/migration-to-26.md
+++ b/docs/builds/guides/migration/migration-to-26.md
@@ -4,6 +4,12 @@ menu-title: Migration to v26.x
order: 98
---
+
+ When updating your CKEditor 5 installation, make sure **all the packages are the same version** to avoid errors.
+
+ For custom builds, you may try removing the `package-lock.json` or `yarn.lock` files (if applicable) and reinstalling all packages before rebuilding the editor. For best results, make sure you use the most recent package versions.
+
+
# Migration to CKEditor 5 v26.0.0
For the entire list of changes introduced in version 26.0.0, see the [changelog for CKEditor 5 v26.0.0](https://github.com/ckeditor/ckeditor5/blob/master/CHANGELOG.md#2600-2021-03-01).
diff --git a/docs/builds/guides/migration/migration-to-27.md b/docs/builds/guides/migration/migration-to-27.md
index df3f8556604..beef00e2ae5 100644
--- a/docs/builds/guides/migration/migration-to-27.md
+++ b/docs/builds/guides/migration/migration-to-27.md
@@ -4,6 +4,12 @@ menu-title: Migration to v27.x
order: 97
---
+
+ When updating your CKEditor 5 installation, make sure **all the packages are the same version** to avoid errors.
+
+ For custom builds, you may try removing the `package-lock.json` or `yarn.lock` files (if applicable) and reinstalling all packages before rebuilding the editor. For best results, make sure you use the most recent package versions.
+
+
# Migration to CKEditor 5 v27.x
## Migration to CKEditor 5 v27.1.0
diff --git a/docs/builds/guides/migration/migration-to-28.md b/docs/builds/guides/migration/migration-to-28.md
index 2df68921abb..097b9e3a41b 100644
--- a/docs/builds/guides/migration/migration-to-28.md
+++ b/docs/builds/guides/migration/migration-to-28.md
@@ -5,6 +5,12 @@ order: 96
modified_at: 2021-06-01
---
+
+ When updating your CKEditor 5 installation, make sure **all the packages are the same version** to avoid errors.
+
+ For custom builds, you may try removing the `package-lock.json` or `yarn.lock` files (if applicable) and reinstalling all packages before rebuilding the editor. For best results, make sure you use the most recent package versions.
+
+
# Migration to CKEditor 5 v28.0.0
For the entire list of changes introduced in version 28.0.0, see the [changelog for CKEditor 5 v28.0.0](https://github.com/ckeditor/ckeditor5/blob/master/CHANGELOG.md#2800-2021-05-31).
diff --git a/docs/builds/guides/migration/migration-to-29.md b/docs/builds/guides/migration/migration-to-29.md
index a97bdb11fe1..56c5dc1a4a0 100644
--- a/docs/builds/guides/migration/migration-to-29.md
+++ b/docs/builds/guides/migration/migration-to-29.md
@@ -6,9 +6,13 @@ order: 95
modified_at: 2021-07-25
---
-# Migration to CKEditor 5 v29.x
+
+ When updating your CKEditor 5 installation, make sure **all the packages are the same version** to avoid errors.
-
+ For custom builds, you may try removing the `package-lock.json` or `yarn.lock` files (if applicable) and reinstalling all packages before rebuilding the editor. For best results, make sure you use the most recent package versions.
+
+
+# Migration to CKEditor 5 v29.x
## Migration to CKEditor 5 v29.1.0
diff --git a/docs/builds/guides/migration/migration-to-30.md b/docs/builds/guides/migration/migration-to-30.md
new file mode 100644
index 00000000000..b9536796c2d
--- /dev/null
+++ b/docs/builds/guides/migration/migration-to-30.md
@@ -0,0 +1,63 @@
+
+---
+category: builds-migration
+menu-title: Migration to v30.x
+order: 94
+modified_at: 2021-09-16
+---
+
+# Migration to CKEditor 5 v30.x
+
+
+ When updating your CKEditor 5 installation, make sure **all the packages are the same version** to avoid errors.
+
+ For custom builds, you may try removing the `package-lock.json` or `yarn.lock` files (if applicable) and reinstalling all packages before rebuilding the editor. For best results, make sure you use the most recent package versions.
+
+
+## Migration to CKEditor 5 v30.0.0
+
+
+
+Listed below are the most important changes that require your attention when upgrading to CKEditor 5 v30.0.0.
+
+### Viewport (toolbar) offset config change
+
+Starting from v30.0.0, the {@link module:core/editor/editorconfig~EditorConfig#toolbar `EditorConfig#toolbar.viewportTopOffset`} config is deprecated.
+
+The new {@link module:core/editor/editorconfig~EditorConfig#ui `EditorConfig#ui.viewportOffset`} editor config allows to set `viewportOffset` from every direction.
+
+```js
+const config = {
+ ui: {
+ viewportOffset: { top: 10, right: 10, bottom: 10, left: 10 }
+ }
+}
+```
+
+Here is the exact change you would need to introduce for proper integration with the new {@link module:core/editor/editorconfig~EditorConfig#ui `EditorConfig#ui.viewportOffset`} config change:
+
+```js
+// Before v30.0.0
+ClassicEditor
+ .create( ..., {
+ // ...
+ toolbar: {
+ items: [ ... ],
+ viewportTopOffset: 100
+ }
+ } )
+
+// Since v30.0.0
+ClassicEditor
+ .create( ..., {
+ // ...
+ toolbar: {
+ items: [ ... ]
+ },
+ ui: {
+ viewportOffset: {
+ top: 100
+ }
+ }
+ } )
+```
diff --git a/docs/framework/guides/contributing/git-commit-message-convention.md b/docs/framework/guides/contributing/git-commit-message-convention.md
index eb200b67bed..05aaf2e15f0 100644
--- a/docs/framework/guides/contributing/git-commit-message-convention.md
+++ b/docs/framework/guides/contributing/git-commit-message-convention.md
@@ -79,6 +79,14 @@ When creating PRs that address specific issues, use the following messages to in
* `See #123` – when the PR only references an issue, but does not close it yet.
* _No reference_ – when the PR does not reference any issue.
+### Methods name syntax
+
+All methods mentioned in the git commit message should use the **#** sign inbetween the class name and the method name. And example of a properly named method:
+
+```
+MarkerCollection#has()
+```
+
### Order of entries
The proper order of sections for a commit message is as follows:
@@ -90,7 +98,7 @@ All entries must be separated with a blank line, otherwise the lines will not be
### Examples of correct and incorrect message formatting
-Example of a proper commit message:
+An example of a proper commit message:
```
Feature (package-name-1): Message 1. Closes: #123
@@ -149,11 +157,11 @@ Tests (widget): Introduced missing tests. Closes #5.
An improvement that is not backward compatible and sent by a non-core contributor. Public API was changed:
```
-Other (utils): Extracted the `utils.foo()` to a separate package. Closes #9.
+Other (utils): Extracted the `utils#foo()` to a separate package. Closes #9.
-Feature (engine): Introduced the `engine.foo()` method. Closes #9.
+Feature (engine): Introduced the `engine#foo()` method. Closes #9.
-MAJOR BREAKING CHANGE (utils): The `utils.foo()` method was moved to the `engine` package. See #9.
+MAJOR BREAKING CHANGE (utils): The `utils#foo()` method was moved to the `engine` package. See #9.
```
For the commits shown above the changelog will look like this:
@@ -166,11 +174,11 @@ Changelog
### MAJOR BREAKING CHANGES [โน๏ธ](https://ckeditor.com/docs/ckeditor5/latest/framework/guides/support/versioning-policy.html#major-and-minor-breaking-changes)
-* **[utils](http://npmjs.com/package/@ckeditor/ckeditor5-utils)**: The `utils.foo()` method was moved to the `engine` package. See [#9](https://github.com/ckeditor/ckeditor5/issue/9).
+* **[utils](http://npmjs.com/package/@ckeditor/ckeditor5-utils)**: The `utils#foo()` method was moved to the `engine` package. See [#9](https://github.com/ckeditor/ckeditor5/issue/9).
### Features
-* **[engine](http://npmjs.com/package/@ckeditor/ckeditor5-engine)**: Introduced the `engine.foo()` method. Thanks to [@CKEditor](https://github.com/CKEditor). Closes [#9](https://github.com/ckeditor/ckeditor5/issue/9). ([e8cc04f](https://github.com/ckeditor/ckeditor5/commit/e8cc04f))
+* **[engine](http://npmjs.com/package/@ckeditor/ckeditor5-engine)**: Introduced the `engine#foo()` method. Thanks to [@CKEditor](https://github.com/CKEditor). Closes [#9](https://github.com/ckeditor/ckeditor5/issue/9). ([e8cc04f](https://github.com/ckeditor/ckeditor5/commit/e8cc04f))
* **[ui](http://npmjs.com/package/@ckeditor/ckeditor5-ui)**: Added support for RTL languages. Closes [#1](https://github.com/ckeditor/ckeditor5/issue/1). ([adc59ed](https://github.com/ckeditor/ckeditor5/commit/adc59ed))
RTL content will now be rendered correctly.
@@ -181,7 +189,7 @@ Changelog
### Other changes
-* **[utils](http://npmjs.com/package/@ckeditor/ckeditor5-utils)**: Extracted the `utils.foo()` to a separate package. Thanks to [@CKEditor](https://github.com/CKEditor). ([e8cc04f](https://github.com/ckeditor/ckeditor5/commit/e8cc04f))
+* **[utils](http://npmjs.com/package/@ckeditor/ckeditor5-utils)**: Extracted the `utils#foo()` to a separate package. Thanks to [@CKEditor](https://github.com/CKEditor). ([e8cc04f](https://github.com/ckeditor/ckeditor5/commit/e8cc04f))
```
## Handling pull requests
diff --git a/docs/index.md b/docs/index.md
index 9c07b82adcf..872b10f19f4 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -3,12 +3,12 @@ toc: false
sitenav: false
feedback-widget: false
meta-title: CKEditor 5 documentation
-meta-description: Learn how to install, integrate, configure and develop CKEditor 5. Browse through API documentation and online samples.
+meta-description: Learn how to install, integrate, update, configure and develop CKEditor 5. Browse through API documentation and online samples.
---
# CKEditor 5 documentation
-
Learn how to install, integrate and configure CKEditor 5 Builds. More complex aspects, like creating custom builds or updating and migrating from an older version are explained here, too.
Learn how to work with CKEditor 5 Framework, customize it, create your own plugins and custom editors, change the UI or even bring your own UI to the editor.
{@link examples/index CKEditor 5 Examples}
Try out all CKEditor 5 Builds. See some of the possible customizations of CKEditor.
{@link features/index CKEditor 5 Features}
Learn about the features available for CKEditor 5 — both the ones included in Builds and a plethora of others.
'
+ );
+ } );
+
+ it( 'should allow nesting downcast conversion', () => {
+ const downcastDispatcher = data.downcastDispatcher;
+ const dataProcessor = data.processor;
+
+ downcastHelpers.elementToElement( { model: 'container', view: 'div' } );
+ downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } );
+
+ data.downcastDispatcher.on( 'insert:caption', ( evt, data, conversionApi ) => {
+ if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) {
+ return;
+ }
+
+ const range = model.createRangeIn( data.item );
+ const viewDocumentFragment = conversionApi.writer.createDocumentFragment();
+
+ // Bind caption model element to the detached view document fragment so all content of the caption
+ // will be downcasted into that document fragment.
+ conversionApi.mapper.bindElements( data.item, viewDocumentFragment );
+
+ for ( const { item } of range ) {
+ const data = {
+ item,
+ range: model.createRangeOn( item )
+ };
+
+ // The following lines are extracted from DowncastDispatcher#_convertInsertWithAttributes().
+
+ const eventName = `insert:${ item.is( '$textProxy' ) ? '$text' : item.name }`;
+
+ downcastDispatcher.fire( eventName, data, conversionApi );
+
+ for ( const key of item.getAttributeKeys() ) {
+ Object.assign( data, {
+ attributeKey: key,
+ attributeOldValue: null,
+ attributeNewValue: data.item.getAttribute( key )
+ } );
+
+ downcastDispatcher.fire( `attribute:${ key }`, data, conversionApi );
+ }
+ }
+
+ // Unbind all the view elements that were downcasted to the document fragment.
+ for ( const child of conversionApi.writer.createRangeIn( viewDocumentFragment ).getItems() ) {
+ conversionApi.mapper.unbindViewElement( child );
+ }
+
+ conversionApi.mapper.unbindViewElement( viewDocumentFragment );
+
+ // Stringify view document fragment to HTML string.
+ const captionText = dataProcessor.toData( viewDocumentFragment );
+
+ if ( captionText ) {
+ const imageViewElement = conversionApi.mapper.toViewElement( data.item.parent );
+
+ conversionApi.writer.setAttribute( 'data-caption', captionText, imageViewElement );
+ }
+ } );
+
+ setData( model, '
foo<$text bold="true">baz$text>
' );
+
+ expect( data.get() ).to.equal( '
' );
+ } );
+ } );
} );
diff --git a/packages/ckeditor5-engine/tests/view/styles/utils.js b/packages/ckeditor5-engine/tests/view/styles/utils.js
index 66c229507a7..f38de73abbc 100644
--- a/packages/ckeditor5-engine/tests/view/styles/utils.js
+++ b/packages/ckeditor5-engine/tests/view/styles/utils.js
@@ -126,6 +126,8 @@ describe( 'Styles utils', () => {
'orange',
// CSS Level 3
'cyan', 'azure', 'wheat',
+ // CSS Level 3 System Colors
+ 'windowtext',
// CSS Level 4
'rebeccapurple'
], isColor );
diff --git a/packages/ckeditor5-find-and-replace/src/findandreplaceediting.js b/packages/ckeditor5-find-and-replace/src/findandreplaceediting.js
index 24a0bcbd961..443dde58694 100644
--- a/packages/ckeditor5-find-and-replace/src/findandreplaceediting.js
+++ b/packages/ckeditor5-find-and-replace/src/findandreplaceediting.js
@@ -94,7 +94,13 @@ export default class FindAndReplaceEditing extends Plugin {
* @inheritDoc
*/
init() {
- this.activeResults = null;
+ /**
+ * The collection of currently highlighted search results.
+ *
+ * @private
+ * @member {module:utils/collection~Collection} #_activeResults
+ */
+ this._activeResults = null;
/**
* An object storing the find and replace state within a given editor instance.
@@ -164,19 +170,19 @@ export default class FindAndReplaceEditing extends Plugin {
const { findCallback, results } = editor.execute( 'find', callbackOrText );
- this.activeResults = results;
+ this._activeResults = results;
// @todo: handle this listener, another copy is in findcommand.js file.
- this.listenTo( model.document, 'change:data', () => onDocumentChange( this.activeResults, model, findCallback ) );
+ this.listenTo( model.document, 'change:data', () => onDocumentChange( this._activeResults, model, findCallback ) );
- return this.activeResults;
+ return this._activeResults;
}
/**
* Stops active results from updating, and clears out the results.
*/
stop() {
- if ( !this.activeResults ) {
+ if ( !this._activeResults ) {
return;
}
@@ -184,10 +190,12 @@ export default class FindAndReplaceEditing extends Plugin {
this.state.clear( this.editor.model );
- this.activeResults = null;
+ this._activeResults = null;
}
/**
+ * Sets up the commands.
+ *
* @private
*/
_defineCommands() {
@@ -199,6 +207,8 @@ export default class FindAndReplaceEditing extends Plugin {
}
/**
+ * Sets up the marker downcast converters for search results highlighting.
+ *
* @private
*/
_defineConverters() {
diff --git a/packages/ckeditor5-find-and-replace/src/findandreplacestate.js b/packages/ckeditor5-find-and-replace/src/findandreplacestate.js
index a6d2a95d8a6..8216824b68c 100644
--- a/packages/ckeditor5-find-and-replace/src/findandreplacestate.js
+++ b/packages/ckeditor5-find-and-replace/src/findandreplacestate.js
@@ -12,13 +12,20 @@ import { ObservableMixin, mix, Collection } from 'ckeditor5/src/utils';
/**
* The object storing find and replace plugin state for a given editor instance.
*
+ * @mixes module:utils/observablemixin~ObservableMixin
*/
export default class FindAndReplaceState {
+ /**
+ * Creates an instance of the state.
+ *
+ * @param {module:engine/model/model~Model} model
+ */
constructor( model ) {
/**
* A collection of find matches.
*
- * @private
+ * @protected
+ * @observable
* @member {module:utils/collection~Collection} #results
*/
this.set( 'results', new Collection() );
@@ -94,6 +101,11 @@ export default class FindAndReplaceState {
} );
}
+ /**
+ * Cleans the state up and removes markers from the model.
+ *
+ * @param {module:engine/model/model~Model} model
+ */
clear( model ) {
this.searchText = '';
diff --git a/packages/ckeditor5-find-and-replace/src/findandreplaceui.js b/packages/ckeditor5-find-and-replace/src/findandreplaceui.js
index bdd43639ce2..4a6dcc4cec0 100644
--- a/packages/ckeditor5-find-and-replace/src/findandreplaceui.js
+++ b/packages/ckeditor5-find-and-replace/src/findandreplaceui.js
@@ -29,6 +29,9 @@ export default class FindAndReplaceUI extends Plugin {
return 'FindAndReplaceUI';
}
+ /**
+ * @inheritDoc
+ */
constructor( editor ) {
super( editor );
@@ -60,7 +63,7 @@ export default class FindAndReplaceUI extends Plugin {
// the default action of the drop-down is executed (i.e. the panel showed up). Otherwise,
// the invisible form/input cannot be focused/selected.
//
- // Each time a dropdown is closed, move the focus back to the editing root (to preserve it)
+ // Each time a dropdown is closed, move the focus back to the find and replace toolbar button
// and let the find and replace editing feature know that all search results can be invalidated
// and no longer should be marked in the content.
dropdown.on( 'change:isOpen', ( event, name, isOpen ) => {
@@ -73,7 +76,7 @@ export default class FindAndReplaceUI extends Plugin {
formView.enableCssTransitions();
} else {
- editor.editing.view.focus();
+ formView.focus();
this.fire( 'searchReseted' );
}
@@ -87,8 +90,10 @@ export default class FindAndReplaceUI extends Plugin {
}
/**
+ * Sets up the find and replace button.
+ *
* @private
- * @param {module:ui/dropdown/dropdownview~DropdownView} buttonView
+ * @param {module:ui/dropdown/dropdownview~DropdownView} dropdown
*/
_setupDropdownButton( dropdown ) {
const editor = this.editor;
@@ -108,6 +113,8 @@ export default class FindAndReplaceUI extends Plugin {
}
/**
+ * Sets up the form view for the find and replace.
+ *
* @private
* @param {module:find-and-replace/ui/findandreplaceformview~FindAndReplaceFormView} formView A related form view.
*/
diff --git a/packages/ckeditor5-find-and-replace/src/findcommand.js b/packages/ckeditor5-find-and-replace/src/findcommand.js
index 4e8cacf5409..0e068d3fc5b 100644
--- a/packages/ckeditor5-find-and-replace/src/findcommand.js
+++ b/packages/ckeditor5-find-and-replace/src/findcommand.js
@@ -20,6 +20,7 @@ export default class FindCommand extends Command {
* Creates a new `FindCommand` instance.
*
* @param {module:core/editor/editor~Editor} editor The editor on which this command will be used.
+ * @param {module:find-and-replace/findandreplacestate~FindAndReplaceState} state An object to hold plugin state.
*/
constructor( editor, state ) {
super( editor );
@@ -27,7 +28,13 @@ export default class FindCommand extends Command {
// The find command is always enabled.
this.isEnabled = true;
- this.state = state;
+ /**
+ * The find and replace state object used for command operations.
+ *
+ * @private
+ * @member {module:find-and-replace/findandreplacestate~FindAndReplaceState} #_state
+ */
+ this._state = state;
// Do not block the command if the editor goes into the read-only mode as it does not impact the data. See #9975.
this.listenTo( editor, 'change:isReadOnly', () => {
@@ -42,6 +49,7 @@ export default class FindCommand extends Command {
* @param {Object} [options]
* @param {Boolean} [options.matchCase=false] If set to `true`, the letter case will be matched.
* @param {Boolean} [options.wholeWords=false] If set to `true`, only whole words that match `callbackOrText` will be matched.
+ *
* @fires execute
*/
execute( callbackOrText, { matchCase, wholeWords } = {} ) {
@@ -54,7 +62,7 @@ export default class FindCommand extends Command {
if ( typeof callbackOrText === 'string' ) {
findCallback = findByTextCallback( callbackOrText, { matchCase, wholeWords } );
- this.state.searchText = callbackOrText;
+ this._state.searchText = callbackOrText;
} else {
findCallback = callbackOrText;
}
@@ -68,16 +76,16 @@ export default class FindCommand extends Command {
currentResults
) ), null );
- this.state.clear( model );
- this.state.results.addMany( Array.from( results ) );
- this.state.highlightedResult = results.get( 0 );
+ this._state.clear( model );
+ this._state.results.addMany( Array.from( results ) );
+ this._state.highlightedResult = results.get( 0 );
if ( typeof callbackOrText === 'string' ) {
- this.state.searchText = callbackOrText;
+ this._state.searchText = callbackOrText;
}
- this.state.matchCase = !!matchCase;
- this.state.matchWholeWords = !!wholeWords;
+ this._state.matchCase = !!matchCase;
+ this._state.matchWholeWords = !!wholeWords;
return {
results,
diff --git a/packages/ckeditor5-find-and-replace/src/findnextcommand.js b/packages/ckeditor5-find-and-replace/src/findnextcommand.js
index da419ee44af..c04d4e4982d 100644
--- a/packages/ckeditor5-find-and-replace/src/findnextcommand.js
+++ b/packages/ckeditor5-find-and-replace/src/findnextcommand.js
@@ -21,6 +21,7 @@ export default class FindNextCommand extends Command {
* Creates a new `FindNextCommand` instance.
*
* @param {module:core/editor/editor~Editor} editor The editor on which this command will be used.
+ * @param {module:find-and-replace/findandreplacestate~FindAndReplaceState} state An object to hold plugin state.
*/
constructor( editor, state ) {
super( editor );
@@ -28,7 +29,7 @@ export default class FindNextCommand extends Command {
/**
* The find and replace state object used for command operations.
*
- * @private
+ * @protected
* @member {module:find-and-replace/findandreplacestate~FindAndReplaceState} #_state
*/
this._state = state;
@@ -45,10 +46,16 @@ export default class FindNextCommand extends Command {
} );
}
+ /**
+ * @inheritDoc
+ */
refresh() {
this.isEnabled = this._state.results.length > 1;
}
+ /**
+ * @inheritDoc
+ */
execute() {
const results = this._state.results;
const currentIndex = results.getIndex( this._state.highlightedResult );
diff --git a/packages/ckeditor5-find-and-replace/src/findpreviouscommand.js b/packages/ckeditor5-find-and-replace/src/findpreviouscommand.js
index 3dbf5f474ce..7a1f910a1e7 100644
--- a/packages/ckeditor5-find-and-replace/src/findpreviouscommand.js
+++ b/packages/ckeditor5-find-and-replace/src/findpreviouscommand.js
@@ -10,13 +10,16 @@
import FindNextCommand from './findnextcommand';
/**
- * The find next command. Moves the highlight to the next search result.
+ * The find previous command. Moves the highlight to the previous search result.
*
* It is used by the {@link module:find-and-replace/findandreplace~FindAndReplace find and replace feature}.
*
* @extends module:find-and-replace/findnextcommand~FindNextCommand
*/
export default class FindPreviousCommand extends FindNextCommand {
+ /**
+ * @inheritDoc
+ */
execute() {
const results = this._state.results;
const currentIndex = results.getIndex( this._state.highlightedResult );
diff --git a/packages/ckeditor5-find-and-replace/src/replaceallcommand.js b/packages/ckeditor5-find-and-replace/src/replaceallcommand.js
index eb8f7712a07..fb7cdc87aee 100644
--- a/packages/ckeditor5-find-and-replace/src/replaceallcommand.js
+++ b/packages/ckeditor5-find-and-replace/src/replaceallcommand.js
@@ -14,7 +14,7 @@ import ReplaceCommand from './replacecommand';
/**
* The replace all command. It is used by the {@link module:find-and-replace/findandreplace~FindAndReplace find and replace feature}.
*
- * @extends module:core/command~Command
+ * @extends module:find-and-replace/replacecommand~ReplaceCommand
*/
export default class ReplaceAllCommand extends ReplaceCommand {
/**
@@ -33,6 +33,8 @@ export default class ReplaceAllCommand extends ReplaceCommand {
* @param {String} newText Text that will be inserted to the editor for each match.
* @param {String|module:utils/collection~Collection} textToReplace Text to be replaced or a collection of matches
* as returned by the find command.
+ *
+ * @fires module:core/command~Command#event:execute
*/
execute( newText, textToReplace ) {
const { editor } = this;
diff --git a/packages/ckeditor5-find-and-replace/src/replacecommand.js b/packages/ckeditor5-find-and-replace/src/replacecommand.js
index fa35e005512..a0ad8421702 100644
--- a/packages/ckeditor5-find-and-replace/src/replacecommand.js
+++ b/packages/ckeditor5-find-and-replace/src/replacecommand.js
@@ -19,6 +19,7 @@ export default class ReplaceCommand extends Command {
* Creates a new `ReplaceCommand` instance.
*
* @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
+ * @param {module:find-and-replace/findandreplacestate~FindAndReplaceState} state An object to hold plugin state.
*/
constructor( editor, state ) {
super( editor );
@@ -29,7 +30,7 @@ export default class ReplaceCommand extends Command {
/**
* The find and replace state object used for command operations.
*
- * @private
+ * @protected
* @member {module:find-and-replace/findandreplacestate~FindAndReplaceState} #_state
*/
this._state = state;
@@ -40,6 +41,8 @@ export default class ReplaceCommand extends Command {
*
* @param {String} replacementText
* @param {Object} result A single result from the find command.
+ *
+ * @fires module:core/command~Command#event:execute
*/
execute( replacementText, result ) {
const { model } = this.editor;
diff --git a/packages/ckeditor5-find-and-replace/src/utils.js b/packages/ckeditor5-find-and-replace/src/utils.js
index 7ba137e04f6..631b4e3fc3e 100644
--- a/packages/ckeditor5-find-and-replace/src/utils.js
+++ b/packages/ckeditor5-find-and-replace/src/utils.js
@@ -4,7 +4,7 @@
*/
/**
- * @module find-and-replace
+ * @module find-and-replace/utils
*/
import { uid, Collection } from 'ckeditor5/src/utils';
@@ -13,11 +13,11 @@ import { escapeRegExp } from 'lodash-es';
/**
* Executes findCallback and updates search results list.
*
- * @param {module:engine/model/range~Range} range
- * @param {module:engine/model/model~Model} model
- * @param {Function} findCallback
+ * @param {module:engine/model/range~Range} range The model range to scan for matches.
+ * @param {module:engine/model/model~Model} model The model.
+ * @param {Function} findCallback The callback that should return `true` if provided text matches the search term.
* @param {module:utils/collection~Collection} [startResults] An optional collection of find matches that the function should
- * starts with. This would be a collection returned by a previous `updateFindResultFromRange()` call.
+ * start with. This would be a collection returned by a previous `updateFindResultFromRange()` call.
* @returns {module:utils/collection~Collection} A collection of objects describing find match.
*
* An example structure:
@@ -79,6 +79,9 @@ export function updateFindResultFromRange( range, model, findCallback, startResu
/**
* Returns text representation of a range. The returned text length should be the same as range length.
* In order to achieve this this function will replace inline elements (text-line) as new line character ("\n").
+ *
+ * @param {module:engine/model/range~Range} range The model range.
+ * @returns {String} The text content of the provided range.
*/
export function rangeToText( range ) {
return Array.from( range.getItems() ).reduce( ( rangeText, node ) => {
@@ -93,6 +96,7 @@ export function rangeToText( range ) {
}, '' );
}
+// Finds the appropriate index in the resultsList Collection.
function findInsertIndex( resultsList, markerToInsert ) {
const result = resultsList.find( ( { marker } ) => {
return markerToInsert.getStart().isBefore( marker.getStart() );
@@ -101,6 +105,7 @@ function findInsertIndex( resultsList, markerToInsert ) {
return result ? resultsList.getIndex( result ) : resultsList.length;
}
+// Maps RegExp match result to find result.
function regexpMatchToFindResult( matchResult ) {
const lastGroupIndex = matchResult.length - 1;
@@ -120,9 +125,10 @@ function regexpMatchToFindResult( matchResult ) {
}
/**
+ * Creates a text matching callback for a specified search term and matching options.
*
- * @param {String} searchTerm
- * @param {Object} [options]
+ * @param {String} searchTerm The search term.
+ * @param {Object} [options] Matching options.
* @param {Boolean} [options.matchCase=false] If set to `true` letter casing will be ignored.
* @param {Boolean} [options.wholeWords=false] If set to `true` only whole words that match `callbackOrText` will be matched.
* @returns {Function}
diff --git a/packages/ckeditor5-find-and-replace/tests/findandreplaceui.js b/packages/ckeditor5-find-and-replace/tests/findandreplaceui.js
index 8e09ea8917f..8966ca86b2f 100644
--- a/packages/ckeditor5-find-and-replace/tests/findandreplaceui.js
+++ b/packages/ckeditor5-find-and-replace/tests/findandreplaceui.js
@@ -122,10 +122,10 @@ describe( 'FindAndReplaceUI', () => {
} );
describe( 'upon dropdown close', () => {
- it( 'the editing view should be focused', () => {
+ it( 'the form should be focused', () => {
dropdown.isOpen = true;
- const spy = sinon.spy( editor.editing.view, 'focus' );
+ const spy = sinon.spy( form, 'focus' );
dropdown.isOpen = false;
diff --git a/packages/ckeditor5-find-and-replace/tests/findcommand.js b/packages/ckeditor5-find-and-replace/tests/findcommand.js
index 7d426588db8..750373bcdf2 100644
--- a/packages/ckeditor5-find-and-replace/tests/findcommand.js
+++ b/packages/ckeditor5-find-and-replace/tests/findcommand.js
@@ -58,7 +58,7 @@ describe( 'FindCommand', () => {
describe( 'state', () => {
it( 'is set to plugin\'s state', () => {
- expect( command.state ).to.equal( editor.plugins.get( 'FindAndReplaceEditing' ).state );
+ expect( command._state ).to.equal( editor.plugins.get( 'FindAndReplaceEditing' ).state );
} );
} );
diff --git a/packages/ckeditor5-image/src/autoimage.js b/packages/ckeditor5-image/src/autoimage.js
index cd9f7324fcf..42a9796956b 100644
--- a/packages/ckeditor5-image/src/autoimage.js
+++ b/packages/ckeditor5-image/src/autoimage.js
@@ -11,6 +11,7 @@ import { Plugin } from 'ckeditor5/src/core';
import { Clipboard } from 'ckeditor5/src/clipboard';
import { LivePosition, LiveRange } from 'ckeditor5/src/engine';
import { Undo } from 'ckeditor5/src/undo';
+import { Delete } from 'ckeditor5/src/typing';
import { global } from 'ckeditor5/src/utils';
import ImageUtils from './imageutils';
@@ -32,7 +33,7 @@ export default class AutoImage extends Plugin {
* @inheritDoc
*/
static get requires() {
- return [ Clipboard, ImageUtils, Undo ];
+ return [ Clipboard, ImageUtils, Undo, Delete ];
}
/**
@@ -173,6 +174,8 @@ export default class AutoImage extends Plugin {
this._positionToInsert.detach();
this._positionToInsert = null;
} );
+
+ editor.plugins.get( 'Delete' ).requestUndoOnBackspace();
}, 100 );
}
}
diff --git a/packages/ckeditor5-image/src/image/ui/utils.js b/packages/ckeditor5-image/src/image/ui/utils.js
index 30d80dc7fa1..8f5e8f9cc2c 100644
--- a/packages/ckeditor5-image/src/image/ui/utils.js
+++ b/packages/ckeditor5-image/src/image/ui/utils.js
@@ -47,7 +47,8 @@ export function getBalloonPositionData( editor ) {
defaultPositions.northArrowSouthEast,
defaultPositions.southArrowNorth,
defaultPositions.southArrowNorthWest,
- defaultPositions.southArrowNorthEast
+ defaultPositions.southArrowNorthEast,
+ defaultPositions.viewportStickyNorth
]
};
}
diff --git a/packages/ckeditor5-image/tests/autoimage.js b/packages/ckeditor5-image/tests/autoimage.js
index a863deeca26..f81964e3648 100644
--- a/packages/ckeditor5-image/tests/autoimage.js
+++ b/packages/ckeditor5-image/tests/autoimage.js
@@ -14,6 +14,7 @@ import Table from '@ckeditor/ckeditor5-table/src/table';
import Typing from '@ckeditor/ckeditor5-typing/src/typing';
import Undo from '@ckeditor/ckeditor5-undo/src/undo';
import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
import Image from '../src/image';
@@ -102,6 +103,30 @@ describe( 'AutoImage - integration', () => {
);
} );
+ it( 'can undo auto-embeding by pressing backspace', () => {
+ const viewDocument = editor.editing.view.document;
+ const deleteEvent = new DomEventData(
+ viewDocument,
+ { preventDefault: sinon.spy() },
+ { direction: 'backward', unit: 'codePoint', sequence: 1 }
+ );
+
+ setData( editor.model, '[]' );
+ pasteHtml( editor, 'http://example.com/image.png' );
+
+ expect( getData( editor.model ) ).to.equal(
+ 'http://example.com/image.png[]'
+ );
+
+ clock.tick( 100 );
+
+ viewDocument.fire( 'delete', deleteEvent );
+
+ expect( getData( editor.model ) ).to.equal(
+ 'http://example.com/image.png[]'
+ );
+ } );
+
describe( 'supported URL', () => {
const supportedURLs = [
'example.com/image.png',
diff --git a/packages/ckeditor5-image/tests/image/ui/utils.js b/packages/ckeditor5-image/tests/image/ui/utils.js
index f0acd66228d..338103dc753 100644
--- a/packages/ckeditor5-image/tests/image/ui/utils.js
+++ b/packages/ckeditor5-image/tests/image/ui/utils.js
@@ -22,7 +22,8 @@ describe( 'Utils', () => {
defaultPositions.northArrowSouthEast,
defaultPositions.southArrowNorth,
defaultPositions.southArrowNorthWest,
- defaultPositions.southArrowNorthEast
+ defaultPositions.southArrowNorthEast,
+ defaultPositions.viewportStickyNorth
];
let editor, converter, selection, balloon, editorElement;
diff --git a/packages/ckeditor5-link/src/autolink.js b/packages/ckeditor5-link/src/autolink.js
index 15d2edf43a2..f5b49c0a7bc 100644
--- a/packages/ckeditor5-link/src/autolink.js
+++ b/packages/ckeditor5-link/src/autolink.js
@@ -8,7 +8,7 @@
*/
import { Plugin } from 'ckeditor5/src/core';
-import { TextWatcher, getLastTextLine } from 'ckeditor5/src/typing';
+import { Delete, TextWatcher, getLastTextLine } from 'ckeditor5/src/typing';
import { addLinkProtocolIfApplicable } from './utils';
@@ -69,6 +69,13 @@ const URL_GROUP_IN_MATCH = 2;
* @extends module:core/plugin~Plugin
*/
export default class AutoLink extends Plugin {
+ /**
+ * @inheritDoc
+ */
+ static get requires() {
+ return [ Delete ];
+ }
+
/**
* @inheritDoc
*/
@@ -226,6 +233,7 @@ export default class AutoLink extends Plugin {
*/
_applyAutoLink( link, range ) {
const model = this.editor.model;
+ const deletePlugin = this.editor.plugins.get( 'Delete' );
if ( !this.isEnabled || !isLinkAllowedOnRange( range, model ) ) {
return;
@@ -236,6 +244,10 @@ export default class AutoLink extends Plugin {
const defaultProtocol = this.editor.config.get( 'link.defaultProtocol' );
const parsedUrl = addLinkProtocolIfApplicable( link, defaultProtocol );
writer.setAttribute( 'linkHref', parsedUrl, range );
+
+ model.enqueueChange( () => {
+ deletePlugin.requestUndoOnBackspace();
+ } );
} );
}
}
diff --git a/packages/ckeditor5-link/tests/autolink.js b/packages/ckeditor5-link/tests/autolink.js
index 6ebe9a1726c..ec2fd9910c1 100644
--- a/packages/ckeditor5-link/tests/autolink.js
+++ b/packages/ckeditor5-link/tests/autolink.js
@@ -10,6 +10,7 @@ import Input from '@ckeditor/ckeditor5-typing/src/input';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import ShiftEnter from '@ckeditor/ckeditor5-enter/src/shiftenter';
import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
+import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
import LinkEditing from '../src/linkediting';
@@ -388,6 +389,23 @@ describe( 'AutoLink', () => {
'[]'
);
} );
+
+ it( 'should undo auto-linking by pressing backspace', () => {
+ const viewDocument = editor.editing.view.document;
+ const deleteEvent = new DomEventData(
+ viewDocument,
+ { preventDefault: sinon.spy() },
+ { direction: 'backward', unit: 'codePoint', sequence: 1 }
+ );
+
+ simulateTyping( ' ' );
+
+ viewDocument.fire( 'delete', deleteEvent );
+
+ expect( getData( model ) ).to.equal(
+ 'https://www.cksource.com []'
+ );
+ } );
} );
describe( 'Code blocks integration', () => {
diff --git a/packages/ckeditor5-media-embed/src/automediaembed.js b/packages/ckeditor5-media-embed/src/automediaembed.js
index d9ceca358c3..51b0d1e7948 100644
--- a/packages/ckeditor5-media-embed/src/automediaembed.js
+++ b/packages/ckeditor5-media-embed/src/automediaembed.js
@@ -10,6 +10,7 @@
import { Plugin } from 'ckeditor5/src/core';
import { LiveRange, LivePosition } from 'ckeditor5/src/engine';
import { Clipboard } from 'ckeditor5/src/clipboard';
+import { Delete } from 'ckeditor5/src/typing';
import { Undo } from 'ckeditor5/src/undo';
import { global } from 'ckeditor5/src/utils';
@@ -29,7 +30,7 @@ export default class AutoMediaEmbed extends Plugin {
* @inheritDoc
*/
static get requires() {
- return [ Clipboard, Undo ];
+ return [ Clipboard, Delete, Undo ];
}
/**
@@ -174,6 +175,8 @@ export default class AutoMediaEmbed extends Plugin {
this._positionToInsert.detach();
this._positionToInsert = null;
} );
+
+ editor.plugins.get( 'Delete' ).requestUndoOnBackspace();
}, 100 );
}
}
diff --git a/packages/ckeditor5-media-embed/tests/automediaembed.js b/packages/ckeditor5-media-embed/tests/automediaembed.js
index fcdf0562d25..5aa36c7d420 100644
--- a/packages/ckeditor5-media-embed/tests/automediaembed.js
+++ b/packages/ckeditor5-media-embed/tests/automediaembed.js
@@ -17,6 +17,7 @@ import Image from '@ckeditor/ckeditor5-image/src/image';
import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption';
import Table from '@ckeditor/ckeditor5-table/src/table';
import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
describe( 'AutoMediaEmbed - integration', () => {
@@ -96,6 +97,30 @@ describe( 'AutoMediaEmbed - integration', () => {
);
} );
+ it( 'can undo auto-embeding by pressing backspace', () => {
+ const viewDocument = editor.editing.view.document;
+ const deleteEvent = new DomEventData(
+ viewDocument,
+ { preventDefault: sinon.spy() },
+ { direction: 'backward', unit: 'codePoint', sequence: 1 }
+ );
+
+ setData( editor.model, '[]' );
+ pasteHtml( editor, 'https://www.youtube.com/watch?v=H08tGjXNHO4' );
+
+ expect( getData( editor.model ) ).to.equal(
+ 'https://www.youtube.com/watch?v=H08tGjXNHO4[]'
+ );
+
+ clock.tick( 100 );
+
+ viewDocument.fire( 'delete', deleteEvent );
+
+ expect( getData( editor.model ) ).to.equal(
+ 'https://www.youtube.com/watch?v=H08tGjXNHO4[]'
+ );
+ } );
+
it( 'works for a full URL (https + "www" sub-domain)', () => {
setData( editor.model, '[]' );
pasteHtml( editor, 'https://www.youtube.com/watch?v=H08tGjXNHO4' );
diff --git a/packages/ckeditor5-mention/docs/features/mentions.md b/packages/ckeditor5-mention/docs/features/mentions.md
index 51e9557e15f..1b224d0b7ec 100644
--- a/packages/ckeditor5-mention/docs/features/mentions.md
+++ b/packages/ckeditor5-mention/docs/features/mentions.md
@@ -9,6 +9,8 @@ menu-title: Mentions
The {@link module:mention/mention~Mention} feature brings support for smart autocompletion based on user input. When a user types a pre-configured marker, such as `@` or `#`, they get autocomplete suggestions in a panel displayed next to the caret. The selected suggestion is then inserted into the content.
+You can read more about possible implementations of the mention feature in a [dedicated blog post](https://ckeditor.com/blog/mentions-in-ckeditor-5-feature-of-the-month/).
+
## Demo
You can type the "@" character to invoke the mention autocomplete UI. The demo below is configured to suggest a static list of names ("Barney", "Lily", "Marshall", "Robin", and "Ted").
diff --git a/packages/ckeditor5-mention/src/mentionui.js b/packages/ckeditor5-mention/src/mentionui.js
index 7f26848b51d..fe9614aad5e 100644
--- a/packages/ckeditor5-mention/src/mentionui.js
+++ b/packages/ckeditor5-mention/src/mentionui.js
@@ -463,7 +463,6 @@ export default class MentionUI extends Plugin {
this._balloon.add( {
view: this._mentionsView,
position: this._getBalloonPanelPositionData( markerMarker, this._mentionsView.position ),
- withArrow: false,
singleViewMode: true
} );
}
@@ -586,7 +585,10 @@ function getBalloonPanelPositions( preferredPosition ) {
return {
top: targetRect.bottom + VERTICAL_SPACING,
left: targetRect.right,
- name: 'caret_se'
+ name: 'caret_se',
+ config: {
+ withArrow: false
+ }
};
},
@@ -595,7 +597,10 @@ function getBalloonPanelPositions( preferredPosition ) {
return {
top: targetRect.top - balloonRect.height - VERTICAL_SPACING,
left: targetRect.right,
- name: 'caret_ne'
+ name: 'caret_ne',
+ config: {
+ withArrow: false
+ }
};
},
@@ -604,7 +609,10 @@ function getBalloonPanelPositions( preferredPosition ) {
return {
top: targetRect.bottom + VERTICAL_SPACING,
left: targetRect.right - balloonRect.width,
- name: 'caret_sw'
+ name: 'caret_sw',
+ config: {
+ withArrow: false
+ }
};
},
@@ -613,7 +621,10 @@ function getBalloonPanelPositions( preferredPosition ) {
return {
top: targetRect.top - balloonRect.height - VERTICAL_SPACING,
left: targetRect.right - balloonRect.width,
- name: 'caret_nw'
+ name: 'caret_nw',
+ config: {
+ withArrow: false
+ }
};
}
};
diff --git a/packages/ckeditor5-mention/tests/mentionui.js b/packages/ckeditor5-mention/tests/mentionui.js
index e6dec3e5ff7..f11640a06f6 100644
--- a/packages/ckeditor5-mention/tests/mentionui.js
+++ b/packages/ckeditor5-mention/tests/mentionui.js
@@ -23,7 +23,7 @@ import MentionsView from '../src/ui/mentionsview';
import { assertCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
describe( 'MentionUI', () => {
- let editor, model, doc, editingView, mentionUI, editorElement, mentionsView, panelView;
+ let editor, model, doc, editingView, mentionUI, editorElement, mentionsView, panelView, clock;
const staticConfig = {
feeds: [
@@ -37,12 +37,14 @@ describe( 'MentionUI', () => {
testUtils.createSinonSandbox();
beforeEach( () => {
+ clock = sinon.useFakeTimers( { now: Date.now() } );
editorElement = document.createElement( 'div' );
document.body.appendChild( editorElement );
} );
afterEach( () => {
sinon.restore();
+ clock.restore();
editorElement.remove();
if ( editor ) {
@@ -196,25 +198,37 @@ describe( 'MentionUI', () => {
expect( caretSouthEast( caretRect, balloonRect ) ).to.deep.equal( {
left: 501,
name: 'caret_se',
- top: 121
+ top: 121,
+ config: {
+ withArrow: false
+ }
} );
expect( caretSouthWest( caretRect, balloonRect ) ).to.deep.equal( {
left: 301,
name: 'caret_sw',
- top: 121
+ top: 121,
+ config: {
+ withArrow: false
+ }
} );
expect( caretNorthEast( caretRect, balloonRect ) ).to.deep.equal( {
left: 501,
name: 'caret_ne',
- top: -53
+ top: -53,
+ config: {
+ withArrow: false
+ }
} );
expect( caretNorthWest( caretRect, balloonRect ) ).to.deep.equal( {
left: 301,
name: 'caret_nw',
- top: -53
+ top: -53,
+ config: {
+ withArrow: false
+ }
} );
} );
} );
@@ -2253,14 +2267,13 @@ describe( 'MentionUI', () => {
function wait( timeout ) {
return () => new Promise( resolve => {
- setTimeout( () => {
- resolve();
- }, timeout );
+ clock.tick( timeout );
+ resolve();
} );
}
- function waitForDebounce() {
- return wait( 180 )();
+ async function waitForDebounce() {
+ return await wait( 180 )();
}
function fireKeyDownEvent( options ) {
diff --git a/packages/ckeditor5-table/src/converters/table-heading-rows-refresh-post-fixer.js b/packages/ckeditor5-table/src/converters/table-heading-rows-refresh-post-fixer.js
index b3810fd6cfb..f3a62c29f8e 100644
--- a/packages/ckeditor5-table/src/converters/table-heading-rows-refresh-post-fixer.js
+++ b/packages/ckeditor5-table/src/converters/table-heading-rows-refresh-post-fixer.js
@@ -29,14 +29,31 @@ function tableHeadingRowsRefreshPostFixer( model ) {
const tablesToRefresh = new Set();
for ( const change of differ.getChanges() ) {
- if ( change.type != 'attribute' ) {
- continue;
- }
+ if ( change.type === 'attribute' ) {
+ const element = change.range.start.nodeAfter;
+
+ if ( element && element.is( 'element', 'table' ) && change.attributeKey === 'headingRows' ) {
+ tablesToRefresh.add( element );
+ }
+ } else {
+ /* istanbul ignore else */
+ if ( change.type === 'insert' || change.type === 'remove' ) {
+ if ( change.name === 'tableRow' ) {
+ const table = change.position.findAncestor( 'table' );
+ const headingRows = table.getAttribute( 'headingRows' ) || 0;
- const element = change.range.start.nodeAfter;
+ if ( change.position.offset < headingRows ) {
+ tablesToRefresh.add( table );
+ }
+ } else if ( change.name === 'tableCell' ) {
+ const table = change.position.findAncestor( 'table' );
+ const headingColumns = table.getAttribute( 'headingColumns' ) || 0;
- if ( element && element.is( 'element', 'table' ) && change.attributeKey == 'headingRows' ) {
- tablesToRefresh.add( element );
+ if ( change.position.offset < headingColumns ) {
+ tablesToRefresh.add( table );
+ }
+ }
+ }
}
}
diff --git a/packages/ckeditor5-table/src/tablecellproperties/ui/tablecellpropertiesview.js b/packages/ckeditor5-table/src/tablecellproperties/ui/tablecellpropertiesview.js
index 4c743cd2c66..8b33d3dfdef 100644
--- a/packages/ckeditor5-table/src/tablecellproperties/ui/tablecellpropertiesview.js
+++ b/packages/ckeditor5-table/src/tablecellproperties/ui/tablecellpropertiesview.js
@@ -799,7 +799,6 @@ export default class TableCellPropertiesView extends View {
label: t( 'Cancel' ),
icon: icons.cancel,
class: 'ck-button-cancel',
- type: 'cancel',
withText: true
} );
diff --git a/packages/ckeditor5-table/src/tableproperties/ui/tablepropertiesview.js b/packages/ckeditor5-table/src/tableproperties/ui/tablepropertiesview.js
index 647fb9f7a71..bfd28e493d2 100644
--- a/packages/ckeditor5-table/src/tableproperties/ui/tablepropertiesview.js
+++ b/packages/ckeditor5-table/src/tableproperties/ui/tablepropertiesview.js
@@ -690,7 +690,6 @@ export default class TablePropertiesView extends View {
label: t( 'Cancel' ),
icon: icons.cancel,
class: 'ck-button-cancel',
- type: 'cancel',
withText: true
} );
diff --git a/packages/ckeditor5-table/src/utils/ui/contextualballoon.js b/packages/ckeditor5-table/src/utils/ui/contextualballoon.js
index 7b81d367624..e624b766849 100644
--- a/packages/ckeditor5-table/src/utils/ui/contextualballoon.js
+++ b/packages/ckeditor5-table/src/utils/ui/contextualballoon.js
@@ -7,7 +7,6 @@
* @module table/utils/ui/contextualballoon
*/
-import { centeredBalloonPositionForLongWidgets } from 'ckeditor5/src/widget';
import { Rect } from 'ckeditor5/src/utils';
import { BalloonPanelView } from 'ckeditor5/src/ui';
@@ -21,12 +20,8 @@ const BALLOON_POSITIONS = [
DEFAULT_BALLOON_POSITIONS.northArrowSouthEast,
DEFAULT_BALLOON_POSITIONS.southArrowNorth,
DEFAULT_BALLOON_POSITIONS.southArrowNorthWest,
- DEFAULT_BALLOON_POSITIONS.southArrowNorthEast
-];
-
-const TABLE_PROPERTIES_BALLOON_POSITIONS = [
- ...BALLOON_POSITIONS,
- centeredBalloonPositionForLongWidgets
+ DEFAULT_BALLOON_POSITIONS.southArrowNorthEast,
+ DEFAULT_BALLOON_POSITIONS.viewportStickyNorth
];
/**
@@ -69,7 +64,7 @@ export function getBalloonTablePositionData( editor ) {
return {
target: editor.editing.view.domConverter.viewToDom( viewTable ),
- positions: TABLE_PROPERTIES_BALLOON_POSITIONS
+ positions: BALLOON_POSITIONS
};
}
diff --git a/packages/ckeditor5-table/tests/_utils/utils.js b/packages/ckeditor5-table/tests/_utils/utils.js
index d646e6c0f91..527abaf74bd 100644
--- a/packages/ckeditor5-table/tests/_utils/utils.js
+++ b/packages/ckeditor5-table/tests/_utils/utils.js
@@ -141,6 +141,10 @@ export function setTableWithObjectAttributes( model, attributes, cellContent ) {
* @returns {String}
*/
export function viewTable( tableData, attributes = {} ) {
+ if ( attributes.headingColumns ) {
+ throw new Error( 'The headingColumns attribute is not supported in viewTable util' );
+ }
+
const headingRows = attributes.headingRows || 0;
const asWidget = !!attributes.asWidget;
diff --git a/packages/ckeditor5-table/tests/converters/downcast.js b/packages/ckeditor5-table/tests/converters/downcast.js
index db215889e78..4741897502b 100644
--- a/packages/ckeditor5-table/tests/converters/downcast.js
+++ b/packages/ckeditor5-table/tests/converters/downcast.js
@@ -8,7 +8,7 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
-import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
import { modelTable, viewTable } from '../_utils/utils';
@@ -479,6 +479,8 @@ describe( 'downcast converters', () => {
writer.insertElement( 'tableCell', row, 'end' );
writer.insertElement( 'tableCell', row, 'end' );
+
+ writer.setAttribute( 'headingRows', 3, table );
} );
assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
@@ -727,7 +729,7 @@ describe( 'downcast converters', () => {
assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
[ { isHeading: true, contents: '00' }, '01' ],
[ { isHeading: true, contents: '10' }, '11' ]
- ], { headingColumns: 1, asWidget: true } ) );
+ ], { asWidget: true } ) );
} );
it( 'should work for changing heading columns to a bigger number', () => {
@@ -763,7 +765,7 @@ describe( 'downcast converters', () => {
assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
[ { isHeading: true, contents: '00' }, '01', '02', '03' ],
[ { isHeading: true, contents: '10' }, '11', '12', '13' ]
- ], { headingColumns: 3, asWidget: true } ) );
+ ], { asWidget: true } ) );
} );
it( 'should work for removing heading columns', () => {
@@ -1032,6 +1034,114 @@ describe( 'downcast converters', () => {
], { headingRows: 2, asWidget: true } ) );
} );
+ it( 'should reorder rows with header correctly - up direction', () => {
+ setModelData( model, modelTable( [
+ [ '00', '01', '02' ],
+ [ '10', '11', '12' ]
+ ], { headingRows: 1 } ) );
+
+ const table = root.getChild( 0 );
+
+ editor.model.change( writer => {
+ writer.move(
+ writer.createRangeOn( table.getChild( 1 ) ),
+ writer.createPositionAt( table, 0 )
+ );
+ } );
+
+ assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+ [ '10', '11', '12' ],
+ [ '00', '01', '02' ]
+ ], { headingRows: 1 } ) );
+
+ assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
+ [ '10', '11', '12' ],
+ [ '00', '01', '02' ]
+ ], { headingRows: 1, asWidget: true } ) );
+ } );
+
+ it( 'should reorder rows with header correctly - down direction', () => {
+ setModelData( model, modelTable( [
+ [ '00', '01', '02' ],
+ [ '10', '11', '12' ]
+ ], { headingRows: 1 } ) );
+
+ const table = root.getChild( 0 );
+
+ editor.model.change( writer => {
+ writer.move(
+ writer.createRangeOn( table.getChild( 0 ) ),
+ writer.createPositionAt( table, 2 )
+ );
+ } );
+
+ assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+ [ '10', '11', '12' ],
+ [ '00', '01', '02' ]
+ ], { headingRows: 1 } ) );
+
+ assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
+ [ '10', '11', '12' ],
+ [ '00', '01', '02' ]
+ ], { headingRows: 1, asWidget: true } ) );
+ } );
+
+ it( 'should reorder columns with header correctly - left direction', () => {
+ setModelData( model, modelTable( [
+ [ '00', '01', '02' ],
+ [ '10', '11', '12' ]
+ ], { headingColumns: 1 } ) );
+
+ const table = root.getChild( 0 );
+
+ editor.model.change( writer => {
+ for ( const tableRow of table.getChildren() ) {
+ writer.move(
+ writer.createRangeOn( tableRow.getChild( 1 ) ),
+ writer.createPositionAt( tableRow, 0 )
+ );
+ }
+ } );
+
+ assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+ [ '01', '00', '02' ],
+ [ '11', '10', '12' ]
+ ], { headingColumns: 1 } ) );
+
+ assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
+ [ { isHeading: true, contents: '01' }, '00', '02' ],
+ [ { isHeading: true, contents: '11' }, '10', '12' ]
+ ], { asWidget: true } ) );
+ } );
+
+ it( 'should reorder columns with header correctly - right direction', () => {
+ setModelData( model, modelTable( [
+ [ '00', '01', '02' ],
+ [ '10', '11', '12' ]
+ ], { headingColumns: 1 } ) );
+
+ const table = root.getChild( 0 );
+
+ editor.model.change( writer => {
+ for ( const tableRow of table.getChildren() ) {
+ writer.move(
+ writer.createRangeOn( tableRow.getChild( 0 ) ),
+ writer.createPositionAt( tableRow, 2 )
+ );
+ }
+ } );
+
+ assertEqualMarkup( getModelData( model, { withoutSelection: true } ), modelTable( [
+ [ '01', '00', '02' ],
+ [ '11', '10', '12' ]
+ ], { headingColumns: 1 } ) );
+
+ assertEqualMarkup( getViewData( view, { withoutSelection: true } ), viewTable( [
+ [ { isHeading: true, contents: '01' }, '00', '02' ],
+ [ { isHeading: true, contents: '11' }, '10', '12' ]
+ ], { asWidget: true } ) );
+ } );
+
it( 'should create renamed cell as a widget', () => {
setModelData( model, modelTable( [ [ '00' ] ] ) );
diff --git a/packages/ckeditor5-table/tests/tablecellproperties/ui/tablecellpropertiesview.js b/packages/ckeditor5-table/tests/tablecellproperties/ui/tablecellpropertiesview.js
index 8deb652117c..1fa114ca2ce 100644
--- a/packages/ckeditor5-table/tests/tablecellproperties/ui/tablecellpropertiesview.js
+++ b/packages/ckeditor5-table/tests/tablecellproperties/ui/tablecellpropertiesview.js
@@ -605,6 +605,7 @@ describe( 'table cell properties', () => {
expect( view.cancelButtonView.label ).to.equal( 'Cancel' );
expect( view.cancelButtonView.withText ).to.be.true;
expect( view.cancelButtonView.class ).to.equal( 'ck-button-cancel' );
+ expect( view.cancelButtonView.type ).to.equal( 'button' );
} );
it( 'should make the cancel button fire the #cancel event when executed', () => {
diff --git a/packages/ckeditor5-table/tests/tableclipboard-copy-cut.js b/packages/ckeditor5-table/tests/tableclipboard-copy-cut.js
index 36b8b0e844c..9fc2d02f3ef 100644
--- a/packages/ckeditor5-table/tests/tableclipboard-copy-cut.js
+++ b/packages/ckeditor5-table/tests/tableclipboard-copy-cut.js
@@ -287,8 +287,8 @@ describe( 'table clipboard', () => {
assertClipboardContentOnMethod( 'copy', viewTable( [
[ '11', '12', '13' ],
[ '21', '22', '23' ],
- [ { contents: '31', isHeading: true }, '32', '33' ] // TODO: bug in viewTable
- ], { headingRows: 2, headingColumns: 1 } ) );
+ [ { contents: '31', isHeading: true }, '32', '33' ]
+ ], { headingRows: 2 } ) );
} );
it( 'should update table heading attributes (selection without headings)', () => {
diff --git a/packages/ckeditor5-table/tests/tableproperties/ui/tablepropertiesview.js b/packages/ckeditor5-table/tests/tableproperties/ui/tablepropertiesview.js
index 00638e6c0c9..01be9ee018f 100644
--- a/packages/ckeditor5-table/tests/tableproperties/ui/tablepropertiesview.js
+++ b/packages/ckeditor5-table/tests/tableproperties/ui/tablepropertiesview.js
@@ -527,6 +527,7 @@ describe( 'table properties', () => {
expect( view.cancelButtonView.label ).to.equal( 'Cancel' );
expect( view.cancelButtonView.withText ).to.be.true;
expect( view.cancelButtonView.class ).to.equal( 'ck-button-cancel' );
+ expect( view.cancelButtonView.type ).to.equal( 'button' );
} );
it( 'should make the cancel button fire the #cancel event when executed', () => {
diff --git a/packages/ckeditor5-table/tests/utils/ui/contextualballoon.js b/packages/ckeditor5-table/tests/utils/ui/contextualballoon.js
index 78cfd744a89..41d8d1e2df4 100644
--- a/packages/ckeditor5-table/tests/utils/ui/contextualballoon.js
+++ b/packages/ckeditor5-table/tests/utils/ui/contextualballoon.js
@@ -13,7 +13,6 @@ import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpa
import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
-import { centeredBalloonPositionForLongWidgets } from '@ckeditor/ckeditor5-widget/src/utils';
import { modelTable } from '../../_utils/utils';
import { getTableCellsContainingSelection } from '../../../src/utils/selection';
import { getBalloonCellPositionData, repositionContextualBalloon } from '../../../src/utils/ui/contextualballoon';
@@ -79,7 +78,8 @@ describe( 'table utils', () => {
defaultPositions.northArrowSouthEast,
defaultPositions.southArrowNorth,
defaultPositions.southArrowNorthWest,
- defaultPositions.southArrowNorthEast
+ defaultPositions.southArrowNorthEast,
+ defaultPositions.viewportStickyNorth
]
} );
} );
@@ -128,7 +128,7 @@ describe( 'table utils', () => {
defaultPositions.southArrowNorth,
defaultPositions.southArrowNorthWest,
defaultPositions.southArrowNorthEast,
- centeredBalloonPositionForLongWidgets
+ defaultPositions.viewportStickyNorth
]
} );
} );
@@ -186,7 +186,8 @@ describe( 'table utils', () => {
defaultPositions.northArrowSouthEast,
defaultPositions.southArrowNorth,
defaultPositions.southArrowNorthWest,
- defaultPositions.southArrowNorthEast
+ defaultPositions.southArrowNorthEast,
+ defaultPositions.viewportStickyNorth
]
} );
} );
diff --git a/packages/ckeditor5-typing/src/delete.js b/packages/ckeditor5-typing/src/delete.js
index 0d921748261..2e66d65fd3c 100644
--- a/packages/ckeditor5-typing/src/delete.js
+++ b/packages/ckeditor5-typing/src/delete.js
@@ -18,6 +18,13 @@ import env from '@ckeditor/ckeditor5-utils/src/env';
* @extends module:core/plugin~Plugin
*/
export default class Delete extends Plugin {
+ /**
+ * Whether pressing backspace should trigger undo action
+ *
+ * @private
+ * @member {Boolean} #_undoOnBackspace
+ */
+
/**
* @inheritDoc
*/
@@ -29,9 +36,12 @@ export default class Delete extends Plugin {
const editor = this.editor;
const view = editor.editing.view;
const viewDocument = view.document;
+ const modelDocument = editor.model.document;
view.addObserver( DeleteObserver );
+ this._undoOnBackspace = false;
+
const deleteForwardCommand = new DeleteCommand( editor, 'forward' );
// Register `deleteForward` command and add `forwardDelete` command as an alias for backward compatibility.
@@ -97,5 +107,33 @@ export default class Delete extends Plugin {
}
} );
}
+
+ if ( this.editor.plugins.has( 'UndoEditing' ) ) {
+ this.listenTo( viewDocument, 'delete', ( evt, data ) => {
+ if ( this._undoOnBackspace && data.direction == 'backward' && data.sequence == 1 && data.unit == 'codePoint' ) {
+ this._undoOnBackspace = false;
+
+ editor.execute( 'undo' );
+
+ data.preventDefault();
+ evt.stop();
+ }
+ }, { context: '$capture' } );
+
+ this.listenTo( modelDocument, 'change', () => {
+ this._undoOnBackspace = false;
+ } );
+ }
+ }
+
+ /**
+ * If the next user action after calling this method is pressing backspace, it would undo the last change.
+ *
+ * Requires {@link module:undo/undoediting~UndoEditing} plugin. If not loaded, does nothing.
+ */
+ requestUndoOnBackspace() {
+ if ( this.editor.plugins.has( 'UndoEditing' ) ) {
+ this._undoOnBackspace = true;
+ }
}
}
diff --git a/packages/ckeditor5-typing/src/deleteobserver.js b/packages/ckeditor5-typing/src/deleteobserver.js
index b8258a89d36..a7c59d0b3a6 100644
--- a/packages/ckeditor5-typing/src/deleteobserver.js
+++ b/packages/ckeditor5-typing/src/deleteobserver.js
@@ -111,7 +111,7 @@ export default class DeleteObserver extends Observer {
* @event module:engine/view/document~Document#event:delete
* @param {module:engine/view/observer/domeventdata~DomEventData} data
* @param {'forward'|'delete'} data.direction The direction in which the deletion should happen.
- * @param {'character'|'word'} data.unit The "amount" of content that should be deleted.
+ * @param {'character'|'codePoint'|'word'} data.unit The "amount" of content that should be deleted.
* @param {Number} data.sequence A number describing which subsequent delete event it is without the key being released.
* If it's 2 or more it means that the key was pressed and hold.
* @param {module:engine/view/selection~Selection} [data.selectionToRemove] View selection which content should be removed. If not set,
diff --git a/packages/ckeditor5-typing/src/texttransformation.js b/packages/ckeditor5-typing/src/texttransformation.js
index 3c89ce7237a..24dfb93b29d 100644
--- a/packages/ckeditor5-typing/src/texttransformation.js
+++ b/packages/ckeditor5-typing/src/texttransformation.js
@@ -19,11 +19,11 @@ const TRANSFORMATIONS = {
trademark: { from: '(tm)', to: 'โข' },
// Mathematical:
- oneHalf: { from: '1/2', to: 'ยฝ' },
- oneThird: { from: '1/3', to: 'โ ' },
- twoThirds: { from: '2/3', to: 'โ ' },
- oneForth: { from: '1/4', to: 'ยผ' },
- threeQuarters: { from: '3/4', to: 'ยพ' },
+ oneHalf: { from: /(^|[^/a-z0-9])(1\/2)([^/a-z0-9])$/i, to: [ null, 'ยฝ', null ] },
+ oneThird: { from: /(^|[^/a-z0-9])(1\/3)([^/a-z0-9])$/i, to: [ null, 'โ ', null ] },
+ twoThirds: { from: /(^|[^/a-z0-9])(2\/3)([^/a-z0-9])$/i, to: [ null, 'โ ', null ] },
+ oneForth: { from: /(^|[^/a-z0-9])(1\/4)([^/a-z0-9])$/i, to: [ null, 'ยผ', null ] },
+ threeQuarters: { from: /(^|[^/a-z0-9])(3\/4)([^/a-z0-9])$/i, to: [ null, 'ยพ', null ] },
lessThanOrEqual: { from: '<=', to: 'โค' },
greaterThanOrEqual: { from: '>=', to: 'โฅ' },
notEqual: { from: '!=', to: 'โ ' },
@@ -74,6 +74,13 @@ const DEFAULT_TRANSFORMATIONS = [
* @extends module:core/plugin~Plugin
*/
export default class TextTransformation extends Plugin {
+ /**
+ * @inheritDoc
+ */
+ static get requires() {
+ return [ 'Delete', 'Input' ];
+ }
+
/**
* @inheritDoc
*/
@@ -117,7 +124,8 @@ export default class TextTransformation extends Plugin {
_enableTransformationWatchers() {
const editor = this.editor;
const model = editor.model;
- const input = editor.plugins.get( 'Input' );
+ const inputPlugin = editor.plugins.get( 'Input' );
+ const deletePlugin = editor.plugins.get( 'Delete' );
const normalizedTransformations = normalizeTransformations( editor.config.get( 'typing.transformations' ) );
const testCallback = text => {
@@ -132,7 +140,7 @@ export default class TextTransformation extends Plugin {
};
const watcherCallback = ( evt, data ) => {
- if ( !input.isInput( data.batch ) ) {
+ if ( !inputPlugin.isInput( data.batch ) ) {
return;
}
@@ -164,6 +172,10 @@ export default class TextTransformation extends Plugin {
changeIndex += replaceWith.length;
}
+
+ model.enqueueChange( () => {
+ deletePlugin.requestUndoOnBackspace();
+ } );
} );
};
diff --git a/packages/ckeditor5-typing/tests/delete.js b/packages/ckeditor5-typing/tests/delete.js
index ed4ef7c56d8..7af7903eb84 100644
--- a/packages/ckeditor5-typing/tests/delete.js
+++ b/packages/ckeditor5-typing/tests/delete.js
@@ -5,8 +5,11 @@
import Delete from '../src/delete';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo';
+import Batch from '@ckeditor/ckeditor5-engine/src/model/batch';
import env from '@ckeditor/ckeditor5-utils/src/env';
import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
@@ -106,12 +109,6 @@ describe( 'Delete feature', () => {
sinon.assert.calledOnce( scrollSpy );
sinon.assert.callOrder( executeSpy, scrollSpy );
} );
-
- function getDomEvent() {
- return {
- preventDefault: sinon.spy()
- };
- }
} );
describe( 'Delete feature - Android', () => {
@@ -222,3 +219,135 @@ describe( 'Delete feature - Android', () => {
} ).not.to.throw();
} );
} );
+
+describe( 'Delete feature - undo by pressing backspace', () => {
+ let element, editor, viewDocument, plugin;
+
+ const deleteEventEventData = {
+ direction: 'backward',
+ unit: 'codePoint',
+ sequence: 1
+ };
+
+ beforeEach( () => {
+ element = document.createElement( 'div' );
+ document.body.appendChild( element );
+
+ return ClassicTestEditor
+ .create( element, { plugins: [ Delete, UndoEditing ] } )
+ .then( newEditor => {
+ editor = newEditor;
+ viewDocument = editor.editing.view.document;
+ plugin = newEditor.plugins.get( 'Delete' );
+ } );
+ } );
+
+ afterEach( () => {
+ element.remove();
+ return editor.destroy();
+ } );
+
+ it( 'executes `undo` once on pressing backspace after requestUndoOnBackspace()', () => {
+ const spy = editor.execute = sinon.spy();
+ const domEvt = getDomEvent();
+ const event = new EventInfo( viewDocument, 'delete' );
+
+ plugin.requestUndoOnBackspace();
+
+ viewDocument.fire( event, new DomEventData( viewDocument, domEvt, deleteEventEventData ) );
+
+ expect( spy.calledOnce ).to.be.true;
+ expect( spy.calledWithMatch( 'undo' ) ).to.be.true;
+
+ expect( event.stop.called ).to.be.true;
+ expect( domEvt.preventDefault.calledOnce ).to.be.true;
+
+ viewDocument.fire( 'delete', new DomEventData( viewDocument, getDomEvent(), deleteEventEventData ) );
+
+ expect( spy.calledTwice ).to.be.true;
+ expect( spy.calledWithMatch( 'delete', {} ) ).to.be.true;
+ } );
+
+ describe( 'does not execute `undo` instead of deleting', () => {
+ const testCases = [
+ {
+ condition: 'it\'s forward deletion',
+ eventData: { direction: 'forward', unit: 'codePoint', sequence: 1 }
+ },
+ {
+ condition: 'the sequence doesn\'t equal 1',
+ eventData: { direction: 'backward', unit: 'codePoint', sequence: 2 }
+ },
+ {
+ condition: 'the unit is not `codePoint`',
+ eventData: { direction: 'backward', unit: 'word', sequence: 1 }
+ }
+ ];
+
+ testCases.forEach( ( { condition, eventData } ) => {
+ it( 'if ' + condition, () => {
+ const spy = editor.execute = sinon.spy();
+
+ plugin.requestUndoOnBackspace();
+
+ viewDocument.fire( 'delete', new DomEventData( viewDocument, getDomEvent(), eventData ) );
+
+ expect( spy.calledOnce ).to.be.true;
+ expect( spy.calledWithMatch( 'undo' ) ).to.be.false;
+ expect( spy.calledWithMatch( 'delete', {} ) ).to.be.true;
+ } );
+ } );
+
+ it( 'if requestUndoOnBackspace() hasn\'t been called', () => {
+ const spy = editor.execute = sinon.spy();
+
+ viewDocument.fire( 'delete', new DomEventData( viewDocument, getDomEvent(), deleteEventEventData ) );
+
+ expect( spy.calledOnce ).to.be.true;
+ expect( spy.calledWithMatch( 'undo' ) ).to.be.false;
+ expect( spy.calledWithMatch( 'delete', {} ) ).to.be.true;
+ } );
+
+ it( 'if `UndoEditing` plugin is not loaded', async () => {
+ await editor.destroy();
+
+ editor = await ClassicTestEditor.create( element, { plugins: [ Delete ] } );
+ viewDocument = editor.editing.view.document;
+ plugin = editor.plugins.get( 'Delete' );
+
+ const spy = editor.execute = sinon.spy();
+
+ plugin.requestUndoOnBackspace();
+
+ viewDocument.fire( 'delete', new DomEventData( viewDocument, getDomEvent(), {
+ direction: 'backward',
+ unit: 'word',
+ sequence: 1
+ } ) );
+
+ expect( spy.calledOnce ).to.be.true;
+ expect( spy.calledWithMatch( 'undo' ) ).to.be.false;
+ expect( spy.calledWithMatch( 'delete', {} ) ).to.be.true;
+ } );
+
+ it( 'after model has changed', () => {
+ const modelDocument = editor.model.document;
+ const spy = editor.execute = sinon.spy();
+
+ plugin.requestUndoOnBackspace();
+
+ modelDocument.fire( 'change', new Batch() );
+ viewDocument.fire( 'delete', new DomEventData( viewDocument, getDomEvent(), deleteEventEventData ) );
+
+ expect( spy.calledOnce ).to.be.true;
+ expect( spy.calledWithMatch( 'undo' ) ).to.be.false;
+ expect( spy.calledWithMatch( 'delete', {} ) ).to.be.true;
+ } );
+ } );
+} );
+
+function getDomEvent() {
+ return {
+ preventDefault: sinon.spy()
+ };
+}
diff --git a/packages/ckeditor5-typing/tests/texttransformation.js b/packages/ckeditor5-typing/tests/texttransformation.js
index 016d6e3bfa4..62f955d37cc 100644
--- a/packages/ckeditor5-typing/tests/texttransformation.js
+++ b/packages/ckeditor5-typing/tests/texttransformation.js
@@ -12,6 +12,8 @@ import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock';
+import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting';
+import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
describe( 'Text transformation feature', () => {
let editorElement, editor, model, doc;
@@ -103,7 +105,13 @@ describe( 'Text transformation feature', () => {
} );
describe( 'mathematical', () => {
- testTransformation( '1/2', 'ยฝ' );
+ testTransformation( '1/2 ', 'ยฝ ', '' );
+ testTransformation( '1/2.', 'ยฝ.', 'A foo ' );
+ testTransformation( '1/2+', 'ยฝ+', '+' );
+ testShouldNotTransform( 'x1/2 ', 'ยฝ ' );
+ testShouldNotTransform( '1/22', 'ยฝ2' );
+ testShouldNotTransform( '11/2', '1ยฝ' );
+ testShouldNotTransform( '1/2A', 'ยฝA' );
testTransformation( '<=', 'โค' );
} );
@@ -186,6 +194,35 @@ describe( 'Text transformation feature', () => {
.to.equal( 'some 1/2 code' );
} );
+ it( 'can undo transformation', () => {
+ setData( model, 'Foo[]' );
+
+ simulateTyping( '(c)' );
+
+ editor.commands.execute( 'undo' );
+
+ expect( getData( model, { withoutSelection: true } ) )
+ .to.equal( 'Foo(c)' );
+ } );
+
+ it( 'can undo transformation by pressing backspace', () => {
+ const viewDocument = editor.editing.view.document;
+ const deleteEvent = new DomEventData(
+ viewDocument,
+ { preventDefault: sinon.spy() },
+ { direction: 'backward', unit: 'codePoint', sequence: 1 }
+ );
+
+ setData( model, 'Foo[]' );
+
+ simulateTyping( '(c)' );
+
+ viewDocument.fire( 'delete', deleteEvent );
+
+ expect( getData( model, { withoutSelection: true } ) )
+ .to.equal( 'Foo(c)' );
+ } );
+
function testTransformation( transformFrom, transformTo, textInParagraph = 'A foo' ) {
it( `should transform "${ transformFrom }" to "${ transformTo }"`, () => {
setData( model, `${ textInParagraph }[]` );
@@ -212,6 +249,31 @@ describe( 'Text transformation feature', () => {
expect( getData( model, { withoutSelection: true } ) )
.to.equal( `${ textInParagraph }${ transformFrom } bar ` );
} );
+
+ it( `should not transform "${ transformFrom }" to "${ transformTo } if not right before selection"`, () => {
+ setData( model, '[]' );
+
+ // Insert text - should not be transformed.
+ model.enqueueChange( model.createBatch(), writer => {
+ writer.insertText( `${ textInParagraph }${ transformFrom }`, doc.selection.focus );
+ } );
+
+ simulateTyping( ' ' );
+
+ expect( getData( model, { withoutSelection: true } ) )
+ .to.equal( `${ textInParagraph }${ transformFrom } ` );
+ } );
+ }
+
+ function testShouldNotTransform( transformFrom, transformTo ) {
+ it( `should not transform "${ transformFrom }" to "${ transformTo }"`, () => {
+ setData( model, '[]' );
+
+ simulateTyping( transformFrom );
+
+ expect( getData( model, { withoutSelection: true } ) )
+ .to.equal( `${ transformFrom }` );
+ } );
}
} );
@@ -386,7 +448,7 @@ describe( 'Text transformation feature', () => {
function createEditorInstance( additionalConfig = {} ) {
return ClassicTestEditor
.create( editorElement, Object.assign( {
- plugins: [ Typing, Paragraph, Bold, TextTransformation, CodeBlock ]
+ plugins: [ Typing, Paragraph, Bold, TextTransformation, CodeBlock, UndoEditing ]
}, additionalConfig ) )
.then( newEditor => {
editor = newEditor;
diff --git a/packages/ckeditor5-ui/src/button/buttonview.js b/packages/ckeditor5-ui/src/button/buttonview.js
index 66244bc1a95..473bfe4042a 100644
--- a/packages/ckeditor5-ui/src/button/buttonview.js
+++ b/packages/ckeditor5-ui/src/button/buttonview.js
@@ -184,7 +184,7 @@ export default class ButtonView extends View {
this.children.add( this.tooltipView );
this.children.add( this.labelView );
- if ( this.withKeystroke ) {
+ if ( this.withKeystroke && this.keystroke ) {
this.children.add( this.keystrokeView );
}
}
diff --git a/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js b/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js
index b9cf9d3cd1a..405585902c5 100644
--- a/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js
+++ b/packages/ckeditor5-ui/src/panel/balloon/balloonpanelview.js
@@ -232,7 +232,8 @@ export default class BalloonPanelView extends View {
defaultPositions.northArrowSouthMiddleWest,
defaultPositions.northArrowSouthMiddleEast,
defaultPositions.northArrowSouthWest,
- defaultPositions.northArrowSouthEast
+ defaultPositions.northArrowSouthEast,
+ defaultPositions.viewportStickyNorth
],
limiter: defaultLimiterElement,
fitInViewport: true
@@ -244,9 +245,11 @@ export default class BalloonPanelView extends View {
// so it is better to use int values.
const left = parseInt( optimalPosition.left );
const top = parseInt( optimalPosition.top );
- const position = optimalPosition.name;
- Object.assign( this, { top, left, position } );
+ const { name: position, config = {} } = optimalPosition;
+ const { withArrow = true } = config;
+
+ Object.assign( this, { top, left, position, withArrow } );
}
/**
@@ -401,7 +404,7 @@ function getDomElement( object ) {
* \|/
* >|-----|<---------------- horizontal offset
*
- * @default 30
+ * @default 25
* @member {Number} module:ui/panel/balloon/balloonpanelview~BalloonPanelView.arrowHorizontalOffset
*/
BalloonPanelView.arrowHorizontalOffset = 25;
@@ -420,11 +423,35 @@ BalloonPanelView.arrowHorizontalOffset = 25;
* -------------------------------
* ^
*
- * @default 15
+ * @default 10
* @member {Number} module:ui/panel/balloon/balloonpanelview~BalloonPanelView.arrowVerticalOffset
*/
BalloonPanelView.arrowVerticalOffset = 10;
+/**
+ * A vertical offset of the balloon panel from the edge of the viewport if sticky.
+ * It helps in accessing toolbar buttons underneath the balloon panel.
+ *
+ * +---------------------------------------------------+
+ * | Target |
+ * | |
+ * | /-- vertical offset |
+ * +-----------------------------V-------------------------+
+ * | Toolbar +-------------+ |
+ * +--------------------| Balloon |--------------------+
+ * | | +-------------+ | |
+ * | | | |
+ * | | | |
+ * | | | |
+ * | +---------------------------------------------------+ |
+ * | Viewport |
+ * +-------------------------------------------------------+
+ *
+ * @default 20
+ * @member {Number} module:ui/panel/balloon/balloonpanelview~BalloonPanelView.stickyVerticalOffset
+ */
+BalloonPanelView.stickyVerticalOffset = 20;
+
/**
* Function used to calculate the optimal position for the balloon.
*
@@ -702,6 +729,22 @@ BalloonPanelView._getOptimalPosition = getOptimalPosition;
* | Balloon |
* +-----------------+
*
+ * * `viewportStickyNorth`
+ *
+ * +---------------------------+
+ * | [ Target ] |
+ * | |
+ * +-----------------------------------+
+ * | | +-----------------+ | |
+ * | | | Balloon | | |
+ * | | +-----------------+ | |
+ * | | | |
+ * | | | |
+ * | | | |
+ * | | | |
+ * | +---------------------------+ |
+ * | Viewport |
+ * +-----------------------------------+
*
* See {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#attachTo}.
*
@@ -710,7 +753,8 @@ BalloonPanelView._getOptimalPosition = getOptimalPosition;
* The name that the position function returns will be reflected in the balloon panel's class that
* controls the placement of the "arrow". See {@link #position} to learn more.
*
- * @member {Object} module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions
+ * @member {Object.}
+ * module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions
*/
BalloonPanelView.defaultPositions = {
@@ -791,6 +835,7 @@ BalloonPanelView.defaultPositions = {
left: targetRect.right - ( balloonRect.width * .25 ) - BalloonPanelView.arrowHorizontalOffset,
name: 'arrow_smw'
} ),
+
northEastArrowSouth: ( targetRect, balloonRect ) => ( {
top: getNorthTop( targetRect, balloonRect ),
left: targetRect.right - balloonRect.width / 2,
@@ -808,6 +853,7 @@ BalloonPanelView.defaultPositions = {
left: targetRect.right - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
name: 'arrow_se'
} ),
+
// ------- South west
southWestArrowNorthWest: ( targetRect, balloonRect ) => ( {
@@ -901,8 +947,24 @@ BalloonPanelView.defaultPositions = {
top: getSouthTop( targetRect, balloonRect ),
left: targetRect.right - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
name: 'arrow_ne'
- } )
+ } ),
+
+ // ------- Sticky
+
+ viewportStickyNorth: ( targetRect, balloonRect, viewportRect ) => {
+ if ( !targetRect.getIntersection( viewportRect ) ) {
+ return null;
+ }
+ return {
+ top: viewportRect.top + BalloonPanelView.stickyVerticalOffset,
+ left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2,
+ name: 'arrowless',
+ config: {
+ withArrow: false
+ }
+ };
+ }
};
// Returns the top coordinate for positions starting with `north*`.
diff --git a/packages/ckeditor5-ui/src/panel/balloon/contextualballoon.js b/packages/ckeditor5-ui/src/panel/balloon/contextualballoon.js
index b3b509d6d9b..05de3305e51 100644
--- a/packages/ckeditor5-ui/src/panel/balloon/contextualballoon.js
+++ b/packages/ckeditor5-ui/src/panel/balloon/contextualballoon.js
@@ -498,11 +498,18 @@ export default class ContextualBalloon extends Plugin {
_getBalloonPosition() {
let position = Array.from( this._visibleStack.values() ).pop().position;
- // Use the default limiter if none has been specified.
- if ( position && !position.limiter ) {
+ if ( position ) {
+ // Use the default limiter if none has been specified.
+ if ( !position.limiter ) {
+ // Don't modify the original options object.
+ position = Object.assign( {}, position, {
+ limiter: this.positionLimiter
+ } );
+ }
+
// Don't modify the original options object.
position = Object.assign( {}, position, {
- limiter: this.positionLimiter
+ viewportOffsetConfig: this.editor.config.get( 'ui.viewportOffset' )
} );
}
diff --git a/packages/ckeditor5-ui/tests/button/buttonview.js b/packages/ckeditor5-ui/tests/button/buttonview.js
index db1922da7bb..2cc6b2287c3 100644
--- a/packages/ckeditor5-ui/tests/button/buttonview.js
+++ b/packages/ckeditor5-ui/tests/button/buttonview.js
@@ -343,7 +343,7 @@ describe( 'ButtonView', () => {
} );
describe( '#keystrokeView', () => {
- it( 'is omited in #children when view#icon is not defined', () => {
+ it( 'is omitted in #children when view#withKeystroke is not set', () => {
view = new ButtonView( locale );
view.render();
@@ -369,7 +369,17 @@ describe( 'ButtonView', () => {
expect( view.keystrokeView.element.textContent ).to.equal( 'Ctrl+A' );
} );
- it( 'usese fancy kesytroke preview on Mac', () => {
+ it( 'is omitted in #children when view#keystroke is not defined', () => {
+ // (#9412)
+ view = new ButtonView( locale );
+ view.withKeystroke = true;
+ view.render();
+
+ expect( view.element.childNodes ).to.have.length( 2 );
+ expect( view.keystrokeView.element ).to.be.null;
+ } );
+
+ it( 'usese fancy keystroke preview on Mac', () => {
testUtils.sinon.stub( env, 'isMac' ).value( true );
view = new ButtonView( locale );
diff --git a/packages/ckeditor5-ui/tests/manual/panel/balloon/balloonpanelview.js b/packages/ckeditor5-ui/tests/manual/panel/balloon/balloonpanelview.js
index c9a90320bbe..85b30fbce53 100644
--- a/packages/ckeditor5-ui/tests/manual/panel/balloon/balloonpanelview.js
+++ b/packages/ckeditor5-ui/tests/manual/panel/balloon/balloonpanelview.js
@@ -10,6 +10,10 @@ import BalloonPanelView from '../../../../src/panel/balloon/balloonpanelview';
const defaultPositions = BalloonPanelView.defaultPositions;
const container = document.querySelector( '#container' );
+// It makes no sense to test the sticky position in this context,
+// thus exclude it from this manual test.
+delete defaultPositions.viewportStickyNorth;
+
let currentHeading = '';
for ( const i in defaultPositions ) {
diff --git a/packages/ckeditor5-ui/tests/manual/tickets/9412/1.html b/packages/ckeditor5-ui/tests/manual/tickets/9412/1.html
new file mode 100644
index 00000000000..c845ad8299f
--- /dev/null
+++ b/packages/ckeditor5-ui/tests/manual/tickets/9412/1.html
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/packages/ckeditor5-ui/tests/manual/tickets/9412/1.js b/packages/ckeditor5-ui/tests/manual/tickets/9412/1.js
new file mode 100644
index 00000000000..aa8ab51a133
--- /dev/null
+++ b/packages/ckeditor5-ui/tests/manual/tickets/9412/1.js
@@ -0,0 +1,44 @@
+/**
+ * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals window, document, console:false */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Heading from '@ckeditor/ckeditor5-heading/src/heading';
+import boldIcon from '@ckeditor/ckeditor5-basic-styles/theme/icons/bold.svg';
+import ButtonView from '../../../../src/button/buttonview';
+
+function customButtonView( editor ) {
+ editor.ui.componentFactory.add( 'customButtonView', locale => {
+ const view = new ButtonView( locale );
+ view.set( {
+ label: 'Custom Button',
+ icon: boldIcon,
+ tooltip: true,
+ withKeystroke: true
+ } );
+
+ return view;
+ } );
+}
+
+ClassicEditor
+ .create( document.querySelector( '#editor' ), {
+ plugins: [
+ Essentials,
+ Paragraph,
+ Heading,
+ customButtonView
+ ],
+ toolbar: [ 'customButtonView' ]
+ } )
+ .then( editor => {
+ window.editor = editor;
+ } )
+ .catch( err => {
+ console.error( err.stack );
+ } );
diff --git a/packages/ckeditor5-ui/tests/manual/tickets/9412/1.md b/packages/ckeditor5-ui/tests/manual/tickets/9412/1.md
new file mode 100644
index 00000000000..95d2c8fd8ff
--- /dev/null
+++ b/packages/ckeditor5-ui/tests/manual/tickets/9412/1.md
@@ -0,0 +1,3 @@
+## Editor crash with keystroke undefined [#9412](https://github.com/ckeditor/ckeditor5/issues/9412)
+
+**Expected**: The editor should create ButtonView without keystrokeView and there shouldn't be any error in console.
\ No newline at end of file
diff --git a/packages/ckeditor5-ui/tests/manual/tickets/9892/1.html b/packages/ckeditor5-ui/tests/manual/tickets/9892/1.html
new file mode 100644
index 00000000000..078019f8193
--- /dev/null
+++ b/packages/ckeditor5-ui/tests/manual/tickets/9892/1.html
@@ -0,0 +1,127 @@
+
+ Sticky header. Table balloon toolbar should not overlap with this header at any point when scrolling down.
+
+
+
+
Other webpage content
+
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Officia itaque eum necessitatibus possimus adipisci mollitia dolor quia molestias voluptate dignissimos? Optio architecto dicta dolorum laborum nam nulla minus aspernatur iste.
+
+
Short table
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Very long table
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Short image
+
+
+
+
+
Very tall image
+
+
+
+
+
+
+
+
+
diff --git a/packages/ckeditor5-ui/tests/manual/tickets/9892/1.js b/packages/ckeditor5-ui/tests/manual/tickets/9892/1.js
new file mode 100644
index 00000000000..fb20b6be929
--- /dev/null
+++ b/packages/ckeditor5-ui/tests/manual/tickets/9892/1.js
@@ -0,0 +1,37 @@
+/**
+ * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals window, document, console:false */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+import TableProperties from '@ckeditor/ckeditor5-table/src/tableproperties';
+import TableCellProperties from '@ckeditor/ckeditor5-table/src/tablecellproperties';
+
+ClassicEditor
+ .create( document.querySelector( '#editor' ), {
+ image: { toolbar: [ 'toggleImageCaption', 'imageTextAlternative' ] },
+ plugins: [ ArticlePluginSet, TableProperties, TableCellProperties ],
+ toolbar: {
+ items: [ 'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ],
+ viewportTopOffset: 100
+ },
+ table: {
+ contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells', '|', 'tableProperties', 'tableCellProperties' ],
+ tableToolbar: [ 'bold', 'italic' ]
+ },
+ ui: {
+ viewportOffset: {
+ top: 100,
+ bottom: 100
+ }
+ }
+ } )
+ .then( editor => {
+ window.editor = editor;
+ } )
+ .catch( err => {
+ console.error( err.stack );
+ } );
diff --git a/packages/ckeditor5-ui/tests/manual/tickets/9892/1.md b/packages/ckeditor5-ui/tests/manual/tickets/9892/1.md
new file mode 100644
index 00000000000..060a650c344
--- /dev/null
+++ b/packages/ckeditor5-ui/tests/manual/tickets/9892/1.md
@@ -0,0 +1,16 @@
+## Table toolbar does not respect viewportTopOffset configuration [#9892](https://github.com/ckeditor/ckeditor5/issues/9892)
+
+### Short table / image
+
+- select a short table or image
+- toolbar balloon shouldn't go under the sticky header if balloon is pinned to the top
+- toolbar balloon shouldn't go under the sticky footer if balloon is pinned to the bottom
+- it's ok for balloon to go under the sticky header if balloon is pinned to the bottom after scrolling down
+
+### Long table / image
+
+- select a long table or image
+- toolbar balloon shouldn't go under the sticky header if balloon is pinned to the top
+- instead it should stick to the bottom of the sticky header, shifted by 20px down, until table / image is scrolled down and there is enough space for the balloon to be pinned to the bottom
+- toolbar balloon shouldn't go under the sticky footer if balloon is pinned to the bottom
+- it's ok for balloon to go under the sticky header if balloon is pinned to the bottom after scrolling down
diff --git a/packages/ckeditor5-ui/tests/manual/tickets/9892/sample-very-tall.jpg b/packages/ckeditor5-ui/tests/manual/tickets/9892/sample-very-tall.jpg
new file mode 100644
index 00000000000..e9d82c3dbc3
Binary files /dev/null and b/packages/ckeditor5-ui/tests/manual/tickets/9892/sample-very-tall.jpg differ
diff --git a/packages/ckeditor5-ui/tests/manual/tickets/9892/sample.jpg b/packages/ckeditor5-ui/tests/manual/tickets/9892/sample.jpg
new file mode 100644
index 00000000000..b77d07e7bff
Binary files /dev/null and b/packages/ckeditor5-ui/tests/manual/tickets/9892/sample.jpg differ
diff --git a/packages/ckeditor5-ui/tests/panel/balloon/balloonpanelview.js b/packages/ckeditor5-ui/tests/panel/balloon/balloonpanelview.js
index 8ad02bdf38f..b48e5083b3d 100644
--- a/packages/ckeditor5-ui/tests/panel/balloon/balloonpanelview.js
+++ b/packages/ckeditor5-ui/tests/panel/balloon/balloonpanelview.js
@@ -9,6 +9,7 @@ import ViewCollection from '../../../src/viewcollection';
import BalloonPanelView from '../../../src/panel/balloon/balloonpanelview';
import ButtonView from '../../../src/button/buttonview';
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import { Rect } from '@ckeditor/ckeditor5-utils';
describe( 'BalloonPanelView', () => {
let view;
@@ -189,7 +190,8 @@ describe( 'BalloonPanelView', () => {
BalloonPanelView.defaultPositions.northArrowSouthMiddleWest,
BalloonPanelView.defaultPositions.northArrowSouthMiddleEast,
BalloonPanelView.defaultPositions.northArrowSouthWest,
- BalloonPanelView.defaultPositions.northArrowSouthEast
+ BalloonPanelView.defaultPositions.northArrowSouthEast,
+ BalloonPanelView.defaultPositions.viewportStickyNorth
],
limiter: document.body,
fitInViewport: true
@@ -209,6 +211,38 @@ describe( 'BalloonPanelView', () => {
expect( view.left ).to.equal( 10 );
} );
+ it( 'should set and override withArrow property', () => {
+ testUtils.sinon.stub( BalloonPanelView, '_getOptimalPosition' ).returns( {
+ top: 10.345,
+ left: 10.345,
+ name: 'position'
+ } );
+
+ view.withArrow = false;
+ view.attachTo( { target, limiter } );
+
+ expect( view.withArrow ).to.be.true;
+
+ view.set( 'withArrow', false );
+ view.attachTo( { target, limiter } );
+
+ expect( view.withArrow ).to.be.true;
+
+ BalloonPanelView._getOptimalPosition.restore();
+
+ testUtils.sinon.stub( BalloonPanelView, '_getOptimalPosition' ).returns( {
+ top: 10.345,
+ left: 10.345,
+ name: 'position',
+ config: {
+ withArrow: false
+ }
+ } );
+
+ view.attachTo( { target, limiter } );
+ expect( view.withArrow ).to.be.false;
+ } );
+
describe( 'limited by limiter element', () => {
beforeEach( () => {
// Mock limiter element dimensions.
@@ -688,34 +722,43 @@ describe( 'BalloonPanelView', () => {
} );
describe( 'defaultPositions', () => {
- let positions, balloonRect, targetRect, arrowHOffset, arrowVOffset;
+ let positions, balloonRect, targetRect, viewportRect, arrowHOffset, arrowVOffset;
beforeEach( () => {
positions = BalloonPanelView.defaultPositions;
arrowHOffset = BalloonPanelView.arrowHorizontalOffset;
arrowVOffset = BalloonPanelView.arrowVerticalOffset;
- targetRect = {
+ viewportRect = new Rect( {
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ width: 200,
+ height: 200
+ } );
+
+ targetRect = new Rect( {
top: 100,
bottom: 200,
left: 100,
right: 200,
width: 100,
height: 100
- };
+ } );
- balloonRect = {
+ balloonRect = new Rect( {
top: 0,
bottom: 0,
left: 0,
right: 0,
width: 50,
height: 50
- };
+ } );
} );
it( 'should have a proper length', () => {
- expect( Object.keys( positions ) ).to.have.length( 30 );
+ expect( Object.keys( positions ) ).to.have.length( 31 );
} );
// ------- North
@@ -969,6 +1012,82 @@ describe( 'BalloonPanelView', () => {
name: 'arrow_nmw'
} );
} );
+
+ // ------- Sticky
+
+ it( 'should define the "viewportStickyNorth" position and return null if not sticky', () => {
+ expect( positions.viewportStickyNorth( targetRect, balloonRect, viewportRect ) ).to.equal( null );
+ } );
+ } );
+
+ describe( 'stickyPositions', () => {
+ let positions, balloonRect, targetRect, viewportRect, stickyOffset;
+
+ beforeEach( () => {
+ positions = BalloonPanelView.defaultPositions;
+ stickyOffset = BalloonPanelView.stickyVerticalOffset;
+
+ balloonRect = new Rect( {
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ width: 50,
+ height: 50
+ } );
+ } );
+
+ it( 'should stick position to the top when top position of the element is above the viewport and the element' +
+ 'area intersects with the viewport area', () => {
+ viewportRect = new Rect( {
+ top: 300,
+ bottom: 800,
+ left: 0,
+ right: 200,
+ width: 0,
+ height: 0
+ } );
+
+ targetRect = new Rect( {
+ top: 200,
+ bottom: 400,
+ left: 50,
+ right: 100,
+ width: 0,
+ height: 0
+ } );
+
+ expect( positions.viewportStickyNorth( targetRect, balloonRect, viewportRect ) ).to.deep.equal( {
+ top: 300 + stickyOffset,
+ left: 25,
+ name: 'arrowless',
+ config: {
+ withArrow: false
+ }
+ } );
+ } );
+
+ it( 'should return null if not sticky because element is fully outside of the viewport', () => {
+ viewportRect = new Rect( {
+ top: 200,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ width: 200,
+ height: 200
+ } );
+
+ targetRect = new Rect( {
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ width: 100,
+ height: 100
+ } );
+
+ expect( positions.viewportStickyNorth( targetRect, balloonRect, viewportRect ) ).to.equal( null );
+ } );
} );
} );
diff --git a/packages/ckeditor5-utils/src/dom/position.js b/packages/ckeditor5-utils/src/dom/position.js
index 969000cc40a..8cc5497597f 100644
--- a/packages/ckeditor5-utils/src/dom/position.js
+++ b/packages/ckeditor5-utils/src/dom/position.js
@@ -13,6 +13,8 @@ import getPositionedAncestor from './getpositionedancestor';
import getBorderWidths from './getborderwidths';
import { isFunction } from 'lodash-es';
+// @if CK_DEBUG_POSITION // import { RectDrawer } from '@ckeditor/ckeditor5-minimap/src/utils';
+
/**
* Calculates the `position: absolute` coordinates of a given element so it can be positioned with respect to the
* target in the visually most efficient way, taking various restrictions like viewport or limiter geometry
@@ -74,10 +76,10 @@ import { isFunction } from 'lodash-es';
* element.style.top = top;
* element.style.left = left;
*
- * @param {module:utils/dom/position~Options} options Positioning options object.
+ * @param {module:utils/dom/position~Options} options The input data and configuration of the helper.
* @returns {module:utils/dom/position~Position}
*/
-export function getOptimalPosition( { element, target, positions, limiter, fitInViewport } ) {
+export function getOptimalPosition( { element, target, positions, limiter, fitInViewport, viewportOffsetConfig } ) {
// If the {@link module:utils/dom/position~Options#target} is a function, use what it returns.
// https://github.com/ckeditor/ckeditor5-utils/issues/157
if ( isFunction( target ) ) {
@@ -94,52 +96,54 @@ export function getOptimalPosition( { element, target, positions, limiter, fitIn
const elementRect = new Rect( element );
const targetRect = new Rect( target );
- let bestPositionRect;
- let bestPositionName;
+ let bestPosition;
+
+ // @if CK_DEBUG_POSITION // RectDrawer.clear();
+ // @if CK_DEBUG_POSITION // RectDrawer.draw( targetRect, { outlineWidth: '5px' }, 'Target' );
+
+ const positionOptions = { targetRect, elementRect, positionedElementAncestor };
// If there are no limits, just grab the very first position and be done with that drama.
if ( !limiter && !fitInViewport ) {
- [ bestPositionName, bestPositionRect ] = getPositionNameAndRect( positions[ 0 ], targetRect, elementRect );
+ bestPosition = new Position( positions[ 0 ], positionOptions );
} else {
const limiterRect = limiter && new Rect( limiter ).getVisible();
- const viewportRect = fitInViewport && new Rect( global.window );
- const bestPosition = getBestPositionNameAndRect( positions, { targetRect, elementRect, limiterRect, viewportRect } );
+ const viewportRect = fitInViewport && getConstrainedViewportRect( viewportOffsetConfig );
- // If there's no best position found, i.e. when all intersections have no area because
- // rects have no width or height, then just use the first available position.
- [ bestPositionName, bestPositionRect ] = bestPosition || getPositionNameAndRect( positions[ 0 ], targetRect, elementRect );
- }
+ // @if CK_DEBUG_POSITION // if ( viewportRect ) {
+ // @if CK_DEBUG_POSITION // RectDrawer.draw( viewportRect, { outlineWidth: '5px' }, 'Viewport' );
+ // @if CK_DEBUG_POSITION // }
- let absoluteRectCoordinates = getAbsoluteRectCoordinates( bestPositionRect );
+ // @if CK_DEBUG_POSITION // if ( limiter ) {
+ // @if CK_DEBUG_POSITION // RectDrawer.draw( limiterRect, { outlineWidth: '5px', outlineColor: 'green' }, 'Visible limiter' );
+ // @if CK_DEBUG_POSITION // }
- if ( positionedElementAncestor ) {
- absoluteRectCoordinates = shiftRectCoordinatesDueToPositionedAncestor( absoluteRectCoordinates, positionedElementAncestor );
+ Object.assign( positionOptions, { limiterRect, viewportRect } );
+
+ // If there's no best position found, i.e. when all intersections have no area because
+ // rects have no width or height, then just use the first available position.
+ bestPosition = getBestPosition( positions, positionOptions ) || new Position( positions[ 0 ], positionOptions );
}
- return {
- left: absoluteRectCoordinates.left,
- top: absoluteRectCoordinates.top,
- name: bestPositionName
- };
+ return bestPosition;
}
-// For given position function, returns a corresponding `Rect` instance.
+// Returns a viewport `Rect` shrunk by the viewport offset config from all sides.
//
// @private
-// @param {Function} position A function returning {@link module:utils/dom/position~Position}.
-// @param {utils/dom/rect~Rect} targetRect A rect of the target.
-// @param {utils/dom/rect~Rect} elementRect A rect of positioned element.
-// @returns {Array|null} An array containing position name and its Rect (or null if position should be ignored).
-function getPositionNameAndRect( position, targetRect, elementRect ) {
- const positionData = position( targetRect, elementRect );
-
- if ( !positionData ) {
- return null;
- }
+// @param {Object} An object containing viewportOffset config.
+// @returns {utils/dom/rect~Rect} A shrunken rect of the viewport.
+function getConstrainedViewportRect( viewportOffsetConfig ) {
+ viewportOffsetConfig = Object.assign( { top: 0, bottom: 0, left: 0, right: 0 }, viewportOffsetConfig );
- const { left, top, name } = positionData;
+ const viewportRect = new Rect( global.window );
- return [ name, elementRect.clone().moveTo( left, top ) ];
+ viewportRect.top += viewportOffsetConfig.top;
+ viewportRect.height -= viewportOffsetConfig.top;
+ viewportRect.bottom -= viewportOffsetConfig.bottom;
+ viewportRect.height -= viewportOffsetConfig.bottom;
+
+ return viewportRect;
}
// For a given array of positioning functions, returns such that provides the best
@@ -148,157 +152,50 @@ function getPositionNameAndRect( position, targetRect, elementRect ) {
// @private
//
// @param {Object} options
-// @param {module:utils/dom/position~Options#positions} positions Functions returning {@link module:utils/dom/position~Position}
-// to be checked, in the order of preference.
+// @param {module:utils/dom/position~Options#positions} positions Functions returning
+// {@link module:utils/dom/position~Position}to be checked, in the order of preference.
// @param {Object} options
// @param {utils/dom/rect~Rect} options.targetRect A rect of the {@link module:utils/dom/position~Options#target}.
-// @param {utils/dom/rect~Rect} options.elementRect A rect of positioned {@link module:utils/dom/position~Options#element}.
+// @param {utils/dom/rect~Rect} options.elementRect A rect of positioned
+// {@link module:utils/dom/position~Options#element}.
// @param {utils/dom/rect~Rect} options.limiterRect A rect of the {@link module:utils/dom/position~Options#limiter}.
-// @param {utils/dom/rect~Rect} options.viewportRect A rect of the viewport.
+// @param {utils/dom/rect~Rect} options.viewportRect A rect of the {@link module:utils/dom/position~Options#viewport}.
//
// @returns {Array} An array containing the name of the position and it's rect.
-function getBestPositionNameAndRect( positions, options ) {
- const { elementRect, viewportRect } = options;
-
- // This is when element is fully visible.
- const elementRectArea = elementRect.getArea();
-
- // Let's calculate intersection areas for positions. It will end early if best match is found.
- const processedPositions = processPositionsToAreas( positions, options );
-
- // First let's check all positions that fully fit in the viewport.
- if ( viewportRect ) {
- const processedPositionsInViewport = processedPositions.filter( ( { viewportIntersectArea } ) => {
- return viewportIntersectArea === elementRectArea;
- } );
-
- // Try to find best position from those which fit completely in viewport.
- const bestPositionData = getBestOfProcessedPositions( processedPositionsInViewport, elementRectArea );
-
- if ( bestPositionData ) {
- return bestPositionData;
- }
- }
-
- // Either there is no viewportRect or there is no position that fits completely in the viewport.
- return getBestOfProcessedPositions( processedPositions, elementRectArea );
-}
-
-// For a given array of positioning functions, calculates intersection areas for them.
-//
-// Note: If some position fully fits into the `limiterRect`, it will be returned early, without further consideration
-// of other positions.
-//
-// @private
-//
-// @param {module:utils/dom/position~Options#positions} positions Functions returning {@link module:utils/dom/position~Position}
-// to be checked, in the order of preference.
-// @param {Object} options
-// @param {utils/dom/rect~Rect} options.targetRect A rect of the {@link module:utils/dom/position~Options#target}.
-// @param {utils/dom/rect~Rect} options.elementRect A rect of positioned {@link module:utils/dom/position~Options#element}.
-// @param {utils/dom/rect~Rect} options.limiterRect A rect of the {@link module:utils/dom/position~Options#limiter}.
-// @param {utils/dom/rect~Rect} options.viewportRect A rect of the viewport.
-//
-// @returns {Array.