8
0
فهرست منبع

Merge tag 'v1.0.0-beta.1' into stable

Release: v1.0.0-beta.1.
Piotrek Koszuliński 7 سال پیش
والد
کامیت
20bcd07dc8
100فایلهای تغییر یافته به همراه1503 افزوده شده و 672 حذف شده
  1. 1 1
      packages/ckeditor5-utils/.eslintrc.js
  2. 15 11
      packages/ckeditor5-utils/.travis.yml
  3. 30 0
      packages/ckeditor5-utils/CHANGELOG.md
  4. 1 1
      packages/ckeditor5-utils/LICENSE.md
  5. 3 1
      packages/ckeditor5-utils/README.md
  6. 1 1
      packages/ckeditor5-utils/dev/tasks/lodash/tasks.js
  7. 6 6
      packages/ckeditor5-utils/package.json
  8. 3 3
      packages/ckeditor5-utils/src/ckeditorerror.js
  9. 112 8
      packages/ckeditor5-utils/src/collection.js
  10. 1 1
      packages/ckeditor5-utils/src/comparearrays.js
  11. 1 1
      packages/ckeditor5-utils/src/config.js
  12. 1 1
      packages/ckeditor5-utils/src/count.js
  13. 1 1
      packages/ckeditor5-utils/src/diff.js
  14. 1 1
      packages/ckeditor5-utils/src/difftochanges.js
  15. 1 1
      packages/ckeditor5-utils/src/dom/createelement.js
  16. 29 42
      packages/ckeditor5-utils/src/dom/emittermixin.js
  17. 1 1
      packages/ckeditor5-utils/src/dom/getancestors.js
  18. 1 1
      packages/ckeditor5-utils/src/dom/getborderwidths.js
  19. 1 1
      packages/ckeditor5-utils/src/dom/getcommonancestor.js
  20. 1 1
      packages/ckeditor5-utils/src/dom/getdatafromelement.js
  21. 1 1
      packages/ckeditor5-utils/src/dom/getpositionedancestor.js
  22. 1 1
      packages/ckeditor5-utils/src/dom/global.js
  23. 1 1
      packages/ckeditor5-utils/src/dom/indexof.js
  24. 1 1
      packages/ckeditor5-utils/src/dom/insertat.js
  25. 0 20
      packages/ckeditor5-utils/src/dom/isdomnode.js
  26. 26 0
      packages/ckeditor5-utils/src/dom/isnode.js
  27. 1 1
      packages/ckeditor5-utils/src/dom/isrange.js
  28. 18 0
      packages/ckeditor5-utils/src/dom/istext.js
  29. 1 1
      packages/ckeditor5-utils/src/dom/iswindow.js
  30. 1 1
      packages/ckeditor5-utils/src/dom/position.js
  31. 13 5
      packages/ckeditor5-utils/src/dom/rect.js
  32. 1 1
      packages/ckeditor5-utils/src/dom/remove.js
  33. 3 4
      packages/ckeditor5-utils/src/dom/scroll.js
  34. 1 1
      packages/ckeditor5-utils/src/dom/setdatainelement.js
  35. 1 1
      packages/ckeditor5-utils/src/dom/tounit.js
  36. 1 1
      packages/ckeditor5-utils/src/elementreplacer.js
  37. 201 153
      packages/ckeditor5-utils/src/emittermixin.js
  38. 3 3
      packages/ckeditor5-utils/src/env.js
  39. 1 1
      packages/ckeditor5-utils/src/eventinfo.js
  40. 1 1
      packages/ckeditor5-utils/src/first.js
  41. 1 1
      packages/ckeditor5-utils/src/focustracker.js
  42. 1 1
      packages/ckeditor5-utils/src/isiterable.js
  43. 27 8
      packages/ckeditor5-utils/src/keyboard.js
  44. 1 1
      packages/ckeditor5-utils/src/keystrokehandler.js
  45. 7 7
      packages/ckeditor5-utils/src/locale.js
  46. 1 1
      packages/ckeditor5-utils/src/log.js
  47. 1 1
      packages/ckeditor5-utils/src/mapsequal.js
  48. 1 1
      packages/ckeditor5-utils/src/mix.js
  49. 1 1
      packages/ckeditor5-utils/src/nth.js
  50. 1 1
      packages/ckeditor5-utils/src/objecttomap.js
  51. 317 267
      packages/ckeditor5-utils/src/observablemixin.js
  52. 1 1
      packages/ckeditor5-utils/src/priorities.js
  53. 1 1
      packages/ckeditor5-utils/src/spy.js
  54. 1 1
      packages/ckeditor5-utils/src/tomap.js
  55. 32 6
      packages/ckeditor5-utils/src/translation-service.js
  56. 4 4
      packages/ckeditor5-utils/src/uid.js
  57. 1 1
      packages/ckeditor5-utils/src/unicode.js
  58. 9 9
      packages/ckeditor5-utils/tests/_utils-tests/normalizehtml.js
  59. 1 1
      packages/ckeditor5-utils/tests/_utils-tests/utils.js
  60. 1 1
      packages/ckeditor5-utils/tests/_utils/locale-mock.js
  61. 1 1
      packages/ckeditor5-utils/tests/_utils/normalizehtml.js
  62. 1 1
      packages/ckeditor5-utils/tests/_utils/utils.js
  63. 3 3
      packages/ckeditor5-utils/tests/ckeditorerror.js
  64. 160 4
      packages/ckeditor5-utils/tests/collection.js
  65. 1 1
      packages/ckeditor5-utils/tests/comparearrays.js
  66. 1 1
      packages/ckeditor5-utils/tests/config.js
  67. 1 1
      packages/ckeditor5-utils/tests/count.js
  68. 1 1
      packages/ckeditor5-utils/tests/diff.js
  69. 1 1
      packages/ckeditor5-utils/tests/difftochanges.js
  70. 1 1
      packages/ckeditor5-utils/tests/dom/createelement.js
  71. 104 1
      packages/ckeditor5-utils/tests/dom/emittermixin.js
  72. 1 1
      packages/ckeditor5-utils/tests/dom/getancestors.js
  73. 1 1
      packages/ckeditor5-utils/tests/dom/getborderwidths.js
  74. 1 1
      packages/ckeditor5-utils/tests/dom/getcommonancestor.js
  75. 1 1
      packages/ckeditor5-utils/tests/dom/getdatafromelement.js
  76. 1 1
      packages/ckeditor5-utils/tests/dom/getpositionedancestor.js
  77. 1 1
      packages/ckeditor5-utils/tests/dom/global.js
  78. 1 1
      packages/ckeditor5-utils/tests/dom/indexof.js
  79. 1 1
      packages/ckeditor5-utils/tests/dom/insertat.js
  80. 32 12
      packages/ckeditor5-utils/tests/dom/isdomnode.js
  81. 1 1
      packages/ckeditor5-utils/tests/dom/isrange.js
  82. 41 0
      packages/ckeditor5-utils/tests/dom/istext.js
  83. 1 1
      packages/ckeditor5-utils/tests/dom/iswindow.js
  84. 1 1
      packages/ckeditor5-utils/tests/dom/position.js
  85. 27 3
      packages/ckeditor5-utils/tests/dom/rect.js
  86. 1 1
      packages/ckeditor5-utils/tests/dom/remove.js
  87. 4 1
      packages/ckeditor5-utils/tests/dom/scroll.js
  88. 1 1
      packages/ckeditor5-utils/tests/dom/setdatainelement.js
  89. 1 1
      packages/ckeditor5-utils/tests/dom/tounit.js
  90. 1 1
      packages/ckeditor5-utils/tests/elementreplacer.js
  91. 169 7
      packages/ckeditor5-utils/tests/emittermixin.js
  92. 4 4
      packages/ckeditor5-utils/tests/env.js
  93. 1 1
      packages/ckeditor5-utils/tests/eventinfo.js
  94. 1 1
      packages/ckeditor5-utils/tests/first.js
  95. 1 1
      packages/ckeditor5-utils/tests/focustracker.js
  96. 1 1
      packages/ckeditor5-utils/tests/isIterable.js
  97. 27 6
      packages/ckeditor5-utils/tests/keyboard.js
  98. 1 1
      packages/ckeditor5-utils/tests/keystrokehandler.js
  99. 5 5
      packages/ckeditor5-utils/tests/locale.js
  100. 1 1
      packages/ckeditor5-utils/tests/lodash.js

+ 1 - 1
packages/ckeditor5-utils/.eslintrc.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 15 - 11
packages/ckeditor5-utils/.travis.yml

@@ -1,24 +1,28 @@
 sudo: required
 dist: trusty
 addons:
+  firefox: "latest"
   apt:
     sources:
-      - google-chrome
+    - google-chrome
     packages:
-      - google-chrome-stable
+    - google-chrome-stable
 language: node_js
 node_js:
-  - "6"
+- '6'
 cache:
-  - node_modules
+- node_modules
 before_install:
-  - export DISPLAY=:99.0
-  - sh -e /etc/init.d/xvfb start
+- export DISPLAY=:99.0
+- sh -e /etc/init.d/xvfb start
 install:
-  - npm install @ckeditor/ckeditor5-dev-tests
-  - ckeditor5-dev-tests-install-dependencies
+- npm install @ckeditor/ckeditor5-dev-tests
+- ckeditor5-dev-tests-install-dependencies
 script:
-  - ckeditor5-dev-tests-travis
+- ckeditor5-dev-tests-travis
 after_success:
-  - codeclimate-test-reporter < coverage/lcov.info
-  - ckeditor5-dev-tests-save-revision
+- ckeditor5-dev-tests-save-revision
+env:
+  global:
+  - secure: FMZImRbIWkU/uUU+1Qxe/7mu/oJJ/At7JEGl+TcpNMHhHBqv4Ikxcwlk32AzkM1SBcNZIVey3sFjLQ8kUiKPpYn7M1ys/vUydOflciM3881OtmXT/Q+zO2CAy+/Ty5HDhXO2hUpJjFTAvQDUEUJgKhs0ZlD9GE8KM+bS3hl9KlWytwtZETArrjaiELI9u7eTErwr7Knu08MxkOgJVQdIvI3JPtzJwdrEzNlEJMAOowPtXwLKmNabpfaSEyLopR9MK8P9SRoVIphTOPWALJZ6SHz8RM2OEX0rRulRMzvi7NllAR7l1cbdAvEzQm8Uyr7nnXEkck2CT4kKHpG22pW7JOeMWK87GJLglia7uPdKTnH1MgLmFhx+jgR06/1xh4CnBhDbAoR5q0We8yw17KqU4FJBFpepapgameJHuqnyZa2vTQUrMwAoSa8p8HSwo/q0eBfu89kAr732fKnCm6IiRp1hVKV4YBCG0Js93zkjeLtgqzQWWVQmJH6xi8DVoZAQhAHpTqf6gJnIARzfigiRg2tSYau3FaBZmXpgeX8DDV6Lc6kCGYYco/NJNlmndw/U1EmnDzaLuZ4AzadqHaS1UsAjDF2LSiG0oNHAP76sN8OswFIrA6dTSnnWn1OJB9x/pupZblIQyDTg8krNmJPjE9snqh/XSVzWUzPTUPRH7kk=
+  - secure: Ll5zMID4/PVtmXN00UKK7z/LKQznWdvv65OAonLoYzoGQqT9CV9RH6lvA1PUVreYW+4tKnR7ZA3PV7mYV7zlwnRomKxYPywdT2pxcREaW4n0SUEFLu4SZICZ1xiO54yjnr6RaOkLa6DJtt/znox0dCQlI33F1neYeqLkpFhCcBWjeq/188/oxnM2CodYea+1pONbbk9zj8o3L+y+ftlAR5b3EYW/bc/7uHl5N+NgWM++O5mh7FzC58QN/4BIGvBvFD67a+5dOGu+5WSn4nYovbTJVrdOMmT0q1Beg6NfhVcTgdprDC5jtTJk7zluHLzzKl2yOZzuBSDD+kbCrovHQI6hP7jHpSPpzjPK/MDLTwiCPkbdhyKt/041bjrUJq4CpOTLsaWebOkkZJGAfgWP64mEaCVgscv+1e6tkNxqjYoNrWsaNZ49s82TUTid3XhjBMXZ3ieoA56dTynD0INPuP6jnkCDqZ/GnRjQ0imNfIGkHVChKKdb3BddVVHh9UJqj74KTKLfGoMOqZy0V2AkbhXqoFKg1El6ax8cglTIR5VRpZ4TQpiKxVLGrIJvNySQpnM2KH58DEN7GAQMmxX7riRfuYmzEixDi+lItkwMARPmNK2rx2+PnGF269SDeK6hoRbug75r1GY9FgOCCLE045TLNIRzpWCdZPkHrayCbUc=

+ 30 - 0
packages/ckeditor5-utils/CHANGELOG.md

@@ -1,6 +1,36 @@
 Changelog
 =========
 
+## [1.0.0-beta.1](https://github.com/ckeditor/ckeditor5-utils/compare/v1.0.0-alpha.2...v1.0.0-beta.1) (2018-03-15)
+
+### Features
+
+* Introduce `bind().toMany()` binding chain in `ObservableMixin`. Closes [#224](https://github.com/ckeditor/ckeditor5-utils/issues/224). ([cfa7d0e](https://github.com/ckeditor/ckeditor5-utils/commit/cfa7d0e))
+* Introduced skipping items when binding collections. Closes [#215](https://github.com/ckeditor/ckeditor5-utils/issues/215). Closes https://github.com/ckeditor/ckeditor5-ui/issues/92. ([6e0d063](https://github.com/ckeditor/ckeditor5-utils/commit/6e0d063))
+
+### Bug fixes
+
+* `Rect.getDomRangeRects()` should not throw if the provided DOM range starts in a text node. Closes [ckeditor/ckeditor5-ui#317](https://github.com/ckeditor/ckeditor5-ui/issues/317). ([bfa55e9](https://github.com/ckeditor/ckeditor5-utils/commit/bfa55e9))
+* Bulletproofed `isDomNode()` helper when used in iframes. Removed `isWindow()` logic from the helper. Closes [#201](https://github.com/ckeditor/ckeditor5-utils/issues/201). ([84ccda2](https://github.com/ckeditor/ckeditor5-utils/commit/84ccda2))
+* Long keystrokes should be handled properly by getEnvKeystrokeText on Mac. Added support for ⇧ and ⌥ modifiers. Closes [#206](https://github.com/ckeditor/ckeditor5-utils/issues/206). ([d8443e2](https://github.com/ckeditor/ckeditor5-utils/commit/d8443e2))
+
+### Other changes
+
+* `ObservableMixin#unbind()` should not throw if used for an attribute which is not bound. Closes [#5](https://github.com/ckeditor/ckeditor5-utils/issues/5). ([848a818](https://github.com/ckeditor/ckeditor5-utils/commit/848a818))
+* Aligned behaviors of `EmitterMixin` methods responsible for adding end removing listeners. Closes [#144](https://github.com/ckeditor/ckeditor5-utils/issues/144). ([460d7f4](https://github.com/ckeditor/ckeditor5-utils/commit/460d7f4))
+
+  The `emitter.on()` now has the same behavior as `emitter.listenTo( emitter )` as well as `emitter.off()` is the same as `emitter.stopListening( emitter )`. This made `emitter.stopListening()` correctly remove all listeners added in any way to it which prevents memory leaks.
+* Aligned code to the new Translation Service ([ckeditor/ckeditor5#624](https://github.com/ckeditor/ckeditor5/issues/624)). ([a51767a](https://github.com/ckeditor/ckeditor5-utils/commit/a51767a))
+* Introduced the `isText()` helper. Closes [#214](https://github.com/ckeditor/ckeditor5-utils/issues/214). ([a9a6bec](https://github.com/ckeditor/ckeditor5-utils/commit/a9a6bec))
+* Renamed `env.mac` to `env.isMac`. Closes [#222](https://github.com/ckeditor/ckeditor5-utils/issues/222). ([dc6b226](https://github.com/ckeditor/ckeditor5-utils/commit/dc6b226))
+* Renamed `isDomNode()` to `isNode()`. Closes [#219](https://github.com/ckeditor/ckeditor5-utils/issues/219). ([1823196](https://github.com/ckeditor/ckeditor5-utils/commit/1823196))
+
+### BREAKING CHANGES
+
+* Renamed `env.mac` to `env.isMac`.
+* `isDomNode()` was renamed to `isNode()`.
+
+
 ## [1.0.0-alpha.2](https://github.com/ckeditor/ckeditor5-utils/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2017-11-14)
 
 ### Bug fixes

+ 1 - 1
packages/ckeditor5-utils/LICENSE.md

@@ -2,7 +2,7 @@ Software License Agreement
 ==========================
 
 **CKEditor 5 Utilities** – https://github.com/ckeditor/ckeditor5-utils <br>
-Copyright (c) 2003-2017, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.
+Copyright (c) 2003-2018, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.
 
 Licensed under the terms of any of the following licenses at your choice:
 

+ 3 - 1
packages/ckeditor5-utils/README.md

@@ -4,7 +4,9 @@ CKEditor 5 utilities
 [![Join the chat at https://gitter.im/ckeditor/ckeditor5](https://badges.gitter.im/ckeditor/ckeditor5.svg)](https://gitter.im/ckeditor/ckeditor5?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 [![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-utils.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-utils)
 [![Build Status](https://travis-ci.org/ckeditor/ckeditor5-utils.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-utils)
-[![Test Coverage](https://codeclimate.com/github/ckeditor/ckeditor5-utils/badges/coverage.svg)](https://codeclimate.com/github/ckeditor/ckeditor5-utils/coverage)
+[![BrowserStack Status](https://www.browserstack.com/automate/badge.svg?badge_key=d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)](https://www.browserstack.com/automate/public-build/d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)
+[![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5-utils/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5-utils?branch=master)
+<br>
 [![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-utils/status.svg)](https://david-dm.org/ckeditor/ckeditor5-utils)
 [![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-utils/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-utils?type=dev)
 

+ 1 - 1
packages/ckeditor5-utils/dev/tasks/lodash/tasks.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 6 - 6
packages/ckeditor5-utils/package.json

@@ -1,19 +1,19 @@
 {
   "name": "@ckeditor/ckeditor5-utils",
-  "version": "1.0.0-alpha.2",
+  "version": "1.0.0-beta.1",
   "description": "Miscellaneous utils used by CKEditor 5.",
   "keywords": [
     "ckeditor5",
     "ckeditor5-lib"
   ],
   "devDependencies": {
-    "@ckeditor/ckeditor5-core": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-engine": "^1.0.0-alpha.2",
+    "@ckeditor/ckeditor5-core": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-engine": "^1.0.0-beta.1",
     "del": "^2.2.0",
-    "eslint": "^4.8.0",
-    "eslint-config-ckeditor5": "^1.0.6",
+    "eslint": "^4.15.0",
+    "eslint-config-ckeditor5": "^1.0.7",
     "husky": "^0.14.3",
-    "lint-staged": "^4.2.3",
+    "lint-staged": "^6.0.0",
     "lodash-cli": "^4"
   },
   "engines": {

+ 3 - 3
packages/ckeditor5-utils/src/ckeditorerror.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -11,7 +11,7 @@
  * URL to the documentation with error codes.
  */
 export const DOCUMENTATION_URL =
-	'https://ckeditor5.github.io/docs/nightly/ckeditor5/latest/framework/guides/support/error-codes.html';
+	'https://docs.ckeditor.com/ckeditor5/latest/framework/guides/support/error-codes.html';
 
 /**
  * The CKEditor error class.
@@ -82,5 +82,5 @@ export function attachLinkToDocumentation( message ) {
 		return message;
 	}
 
-	return message + ` Read more: ${ DOCUMENTATION_URL }#${ matchedErrorName[ 1 ] }\n`;
+	return message + ` Read more: ${ DOCUMENTATION_URL }#error-${ matchedErrorName[ 1 ] }\n`;
 }

+ 112 - 8
packages/ckeditor5-utils/src/collection.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -80,6 +80,14 @@ export default class Collection {
 		 */
 		this._bindToInternalToExternalMap = new WeakMap();
 
+		/**
+		 * Stores indexes of skipped items from bound external collection.
+		 *
+		 * @private
+		 * @member {Array}
+		 */
+		this._skippedIndexesFromExternal = [];
+
 		/**
 		 * A collection instance this collection is bound to as a result
 		 * of calling {@link #bindTo} method.
@@ -271,7 +279,7 @@ export default class Collection {
 		this._bindToInternalToExternalMap.delete( item );
 		this._bindToExternalToInternalMap.delete( externalItem );
 
-		this.fire( 'remove', item );
+		this.fire( 'remove', item, index );
 
 		return item;
 	}
@@ -402,9 +410,28 @@ export default class Collection {
 	 *		console.log( target.get( 0 ).value ); // 'foo'
 	 *		console.log( target.get( 1 ).value ); // 'bar'
 	 *
+	 * It's possible to skip specified items by returning falsy value:
+	 *
+	 *		const source = new Collection();
+	 *		const target = new Collection();
+	 *
+	 *		target.bindTo( source ).using( item => {
+	 *			if ( item.hidden ) {
+	 *				return null;
+	 *			}
+	 *
+	 *			return item;
+	 *		} );
+	 *
+	 *		source.add( { hidden: true } );
+	 *		source.add( { hidden: false } );
+	 *
+	 *		console.log( source.length ); // 2
+	 *		console.log( target.length ); // 1
+	 *
 	 * **Note**: {@link #clear} can be used to break the binding.
 	 *
-	 * @param {module:utils/collection~Collection} collection A collection to be bound.
+	 * @param {module:utils/collection~Collection} externalCollection A collection to be bound.
 	 * @returns {Object}
 	 * @returns {module:utils/collection~Collection#bindTo#as} return.as
 	 * @returns {module:utils/collection~Collection#bindTo#using} return.using
@@ -476,33 +503,109 @@ export default class Collection {
 			} else {
 				const item = factory( externalItem );
 
+				// When there is no item we need to remember skipped index first and then we can skip this item.
+				if ( !item ) {
+					this._skippedIndexesFromExternal.push( index );
+
+					return;
+				}
+
+				// Lets try to put item at the same index as index in external collection
+				// but when there are a skipped items in one or both collections we need to recalculate this index.
+				let finalIndex = index;
+
+				// When we try to insert item after some skipped items from external collection we need
+				// to include this skipped items and decrease index.
+				//
+				// For the following example:
+				// external -> [ 'A', 'B - skipped for internal', 'C - skipped for internal' ]
+				// internal -> [ A ]
+				//
+				// Another item is been added at the end of external collection:
+				// external.add( 'D' )
+				// external -> [ 'A', 'B - skipped for internal', 'C - skipped for internal', 'D' ]
+				//
+				// We can't just add 'D' to internal at the same index as index in external because
+				// this will produce empty indexes what is invalid:
+				// internal -> [ 'A', empty, empty, 'D' ]
+				//
+				// So we need to include skipped items and decrease index
+				// internal -> [ 'A', 'D' ]
+				for ( const skipped of this._skippedIndexesFromExternal ) {
+					if ( index > skipped ) {
+						finalIndex--;
+					}
+				}
+
+				// We need to take into consideration that external collection could skip some items from
+				// internal collection.
+				//
+				// For the following example:
+				// internal -> [ 'A', 'B - skipped for external', 'C - skipped for external' ]
+				// external -> [ A ]
+				//
+				// Another item is been added at the end of external collection:
+				// external.add( 'D' )
+				// external -> [ 'A', 'D' ]
+				//
+				// We need to include skipped items and place new item after them:
+				// internal -> [ 'A', 'B - skipped for external', 'C - skipped for external', 'D' ]
+				for ( const skipped of externalCollection._skippedIndexesFromExternal ) {
+					if ( finalIndex >= skipped ) {
+						finalIndex++;
+					}
+				}
+
 				this._bindToExternalToInternalMap.set( externalItem, item );
 				this._bindToInternalToExternalMap.set( item, externalItem );
-
-				this.add( item, index );
+				this.add( item, finalIndex );
+
+				// After adding new element to internal collection we need update indexes
+				// of skipped items in external collection.
+				for ( let i = 0; i < externalCollection._skippedIndexesFromExternal.length; i++ ) {
+					if ( finalIndex <= externalCollection._skippedIndexesFromExternal[ i ] ) {
+						externalCollection._skippedIndexesFromExternal[ i ]++;
+					}
+				}
 			}
 		};
 
 		// Load the initial content of the collection.
 		for ( const externalItem of externalCollection ) {
-			addItem( null, externalItem );
+			addItem( null, externalItem, externalCollection.getIndex( externalItem ) );
 		}
 
 		// Synchronize the with collection as new items are added.
 		this.listenTo( externalCollection, 'add', addItem );
 
 		// Synchronize the with collection as new items are removed.
-		this.listenTo( externalCollection, 'remove', ( evt, externalItem ) => {
+		this.listenTo( externalCollection, 'remove', ( evt, externalItem, index ) => {
 			const item = this._bindToExternalToInternalMap.get( externalItem );
 
 			if ( item ) {
 				this.remove( item );
 			}
+
+			// After removing element from external collection we need update/remove indexes
+			// of skipped items in internal collection.
+			this._skippedIndexesFromExternal = this._skippedIndexesFromExternal.reduce( ( result, skipped ) => {
+				if ( index < skipped ) {
+					result.push( skipped - 1 );
+				}
+
+				if ( index > skipped ) {
+					result.push( skipped );
+				}
+
+				return result;
+			}, [] );
 		} );
 	}
 
 	/**
-	 * Collection iterator.
+	 * Iterable interface.
+	 *
+	 * @returns {Iterable.<*>}
 	 */
 	[ Symbol.iterator ]() {
 		return this._items[ Symbol.iterator ]();
@@ -520,6 +623,7 @@ export default class Collection {
 	 *
 	 * @event remove
 	 * @param {Object} item The removed item.
+	 * @param {Number} index Index from which item was removed.
 	 */
 }
 

+ 1 - 1
packages/ckeditor5-utils/src/comparearrays.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/config.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/count.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/diff.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/difftochanges.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/dom/createelement.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 29 - 42
packages/ckeditor5-utils/src/dom/emittermixin.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -10,7 +10,8 @@
 import { default as EmitterMixin, _getEmitterListenedTo, _setEmitterId } from '../emittermixin';
 import uid from '../uid';
 import extend from '../lib/lodash/extend';
-import isDomNode from './isdomnode';
+import isNode from './isnode';
+import isWindow from './iswindow';
 
 /**
  * Mixin that injects the DOM events API into its host. It provides the API
@@ -30,7 +31,7 @@ import isDomNode from './isdomnode';
  *			console.log( evt, domEvt );
  *		} );
  *
- * @mixin module:utils/dom/emittermixin~EmitterMixin
+ * @mixin EmitterMixin
  * @mixes module:utils/emittermixin~EmitterMixin
  * @implements module:utils/dom/emittermixin~Emitter
  */
@@ -48,20 +49,20 @@ const DomEmitterMixin = extend( {}, EmitterMixin, {
 	 * order they were added.
 	 * @param {Boolean} [options.useCapture=false] Indicates that events of this type will be dispatched to the registered
 	 * listener before being dispatched to any EventTarget beneath it in the DOM tree.
-	 *
-	 * @method module:utils/dom/emittermixin~EmitterMixin#listenTo
 	 */
-	listenTo( ...args ) {
-		const emitter = args[ 0 ];
-
+	listenTo( emitter, ...rest ) {
 		// Check if emitter is an instance of DOM Node. If so, replace the argument with
 		// corresponding ProxyEmitter (or create one if not existing).
-		if ( isDomNode( emitter ) ) {
-			args[ 0 ] = this._getProxyEmitter( emitter ) || new ProxyEmitter( emitter );
+		if ( isNode( emitter ) || isWindow( emitter ) ) {
+			const proxy = this._getProxyEmitter( emitter ) || new ProxyEmitter( emitter );
+
+			proxy.attach( ...rest );
+
+			emitter = proxy;
 		}
 
 		// Execute parent class method with Emitter (or ProxyEmitter) instance.
-		EmitterMixin.listenTo.apply( this, args );
+		EmitterMixin.listenTo.call( this, emitter, ...rest );
 	},
 
 	/**
@@ -78,14 +79,10 @@ const DomEmitterMixin = extend( {}, EmitterMixin, {
 	 * for all events from `emitter`.
 	 * @param {Function} [callback] (Requires the `event`) The function to be removed from the call list for the given
 	 * `event`.
-	 *
-	 * @method module:utils/dom/emittermixin~EmitterMixin#stopListening
 	 */
-	stopListening( ...args ) {
-		const emitter = args[ 0 ];
-
+	stopListening( emitter, event, callback ) {
 		// Check if emitter is an instance of DOM Node. If so, replace the argument with corresponding ProxyEmitter.
-		if ( isDomNode( emitter ) ) {
+		if ( isNode( emitter ) || isWindow( emitter ) ) {
 			const proxy = this._getProxyEmitter( emitter );
 
 			// Element has no listeners.
@@ -93,19 +90,23 @@ const DomEmitterMixin = extend( {}, EmitterMixin, {
 				return;
 			}
 
-			args[ 0 ] = proxy;
+			emitter = proxy;
 		}
 
 		// Execute parent class method with Emitter (or ProxyEmitter) instance.
-		EmitterMixin.stopListening.apply( this, args );
+		EmitterMixin.stopListening.call( this, emitter, event, callback );
+
+		if ( emitter instanceof ProxyEmitter ) {
+			emitter.detach( event );
+		}
 	},
 
 	/**
 	 * Retrieves ProxyEmitter instance for given DOM Node residing in this Host.
 	 *
+	 * @pivate
 	 * @param {Node} node DOM Node of the ProxyEmitter.
-	 * @method module:utils/dom/emittermixin~EmitterMixin#_getProxyEmitter
-	 * @return {module:utils/dom/emittermixin~ProxyEmitter} ProxyEmitter instance or null.
+	 * @returns {module:utils/dom/emittermixin~ProxyEmitter} ProxyEmitter instance or null.
 	 */
 	_getProxyEmitter( node ) {
 		return _getEmitterListenedTo( this, getNodeUID( node ) );
@@ -172,21 +173,14 @@ extend( ProxyEmitter.prototype, EmitterMixin, {
 	 * It attaches a native DOM listener to the DOM Node. When fired,
 	 * a corresponding Emitter event will also fire with DOM Event object as an argument.
 	 *
+	 * @method module:utils/dom/emittermixin~ProxyEmitter#attach
 	 * @param {String} event The name of the event.
 	 * @param {Function} callback The function to be called on event.
 	 * @param {Object} [options={}] Additional options.
-	 * @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
-	 * the priority value the sooner the callback will be fired. Events having the same priority are called in the
-	 * order they were added.
 	 * @param {Boolean} [options.useCapture=false] Indicates that events of this type will be dispatched to the registered
 	 * listener before being dispatched to any EventTarget beneath it in the DOM tree.
-	 *
-	 * @method module:utils/dom/emittermixin~ProxyEmitter#on
 	 */
-	on( event, callback, options = {} ) {
-		// Execute parent class method first.
-		EmitterMixin.on.call( this, event, callback, options );
-
+	attach( event, callback, options = {} ) {
 		// If the DOM Listener for given event already exist it is pointless
 		// to attach another one.
 		if ( this._domListeners && this._domListeners[ event ] ) {
@@ -210,20 +204,15 @@ extend( ProxyEmitter.prototype, EmitterMixin, {
 	/**
 	 * Stops executing the callback on the given event.
 	 *
+	 * @method module:utils/dom/emittermixin~ProxyEmitter#detach
 	 * @param {String} event The name of the event.
-	 * @param {Function} callback The function to stop being called.
-	 *
-	 * @method module:utils/dom/emittermixin~ProxyEmitter#off
 	 */
-	off( event, callback ) {
-		// Execute parent class method first.
-		EmitterMixin.off.call( this, event, callback );
-
+	detach( event ) {
 		let events;
 
 		// Remove native DOM listeners which are orphans. If no callbacks
 		// are awaiting given event, detach native DOM listener from DOM Node.
-		// See: {@link on}.
+		// See: {@link attach}.
 
 		if ( this._domListeners[ event ] && ( !( events = this._events[ event ] ) || !events.callbacks.length ) ) {
 			this._domListeners[ event ].removeListener();
@@ -231,13 +220,11 @@ extend( ProxyEmitter.prototype, EmitterMixin, {
 	},
 
 	/**
-	 * Create a native DOM listener callback. When the native DOM event
+	 * Creates a native DOM listener callback. When the native DOM event
 	 * is fired it will fire corresponding event on this ProxyEmitter.
 	 * Note: A native DOM Event is passed as an argument.
 	 *
 	 * @private
-	 * @param {String} event
-	 *
 	 * @method module:utils/dom/emittermixin~ProxyEmitter#_createDomListener
 	 * @param {String} event The name of the event.
 	 * @param {Boolean} useCapture Indicates whether the listener was created for capturing event.
@@ -250,7 +237,7 @@ extend( ProxyEmitter.prototype, EmitterMixin, {
 
 		// Supply the DOM listener callback with a function that will help
 		// detach it from the DOM Node, when it is no longer necessary.
-		// See: {@link off}.
+		// See: {@link detach}.
 		domListener.removeListener = () => {
 			this._domNode.removeEventListener( event, domListener, useCapture );
 			delete this._domListeners[ event ];

+ 1 - 1
packages/ckeditor5-utils/src/dom/getancestors.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/dom/getborderwidths.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/dom/getcommonancestor.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/dom/getdatafromelement.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/dom/getpositionedancestor.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/dom/global.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/dom/indexof.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/dom/insertat.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 0 - 20
packages/ckeditor5-utils/src/dom/isdomnode.js

@@ -1,20 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module utils/dom/isdomnode
- */
-
-import isNative from '../lib/lodash/isNative';
-
-/**
- * Checks if the object is a native DOM Node.
- *
- * @param {*} obj
- * @returns {Boolean}
- */
-export default function isDomNode( obj ) {
-	return !!( obj && isNative( obj.addEventListener ) );
-}

+ 26 - 0
packages/ckeditor5-utils/src/dom/isnode.js

@@ -0,0 +1,26 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module utils/dom/isnode
+ */
+
+/**
+ * Checks if the object is a native DOM Node.
+ *
+ * @param {*} obj
+ * @returns {Boolean}
+ */
+export default function isNode( obj ) {
+	if ( obj ) {
+		if ( obj.defaultView ) {
+			return obj instanceof obj.defaultView.Document;
+		} else if ( obj.ownerDocument && obj.ownerDocument.defaultView ) {
+			return obj instanceof obj.ownerDocument.defaultView.Node;
+		}
+	}
+
+	return false;
+}

+ 1 - 1
packages/ckeditor5-utils/src/dom/isrange.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 18 - 0
packages/ckeditor5-utils/src/dom/istext.js

@@ -0,0 +1,18 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module utils/dom/istext
+ */
+
+/**
+ * Checks if the object is a native DOM Text node.
+ *
+ * @param {*} obj
+ * @returns {Boolean}
+ */
+export default function isText( obj ) {
+	return Object.prototype.toString.call( obj ) == '[object Text]';
+}

+ 1 - 1
packages/ckeditor5-utils/src/dom/iswindow.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/dom/position.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 13 - 5
packages/ckeditor5-utils/src/dom/rect.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -12,6 +12,7 @@ import isWindow from './iswindow';
 import isElement from '../lib/lodash/isElement';
 import getBorderWidths from './getborderwidths';
 import log from '../log';
+import isText from './istext';
 
 /**
  * A helper class representing a `ClientRect` object, e.g. value returned by
@@ -361,12 +362,19 @@ export default class Rect {
 		// If there's no client rects for the Range, use parent container's bounding rect
 		// instead and adjust rect's width to simulate the actual geometry of such range.
 		// https://github.com/ckeditor/ckeditor5-utils/issues/153
+		// https://github.com/ckeditor/ckeditor5-ui/issues/317
 		else {
-			const startContainerRect = new Rect( range.startContainer.getBoundingClientRect() );
-			startContainerRect.right = startContainerRect.left;
-			startContainerRect.width = 0;
+			let startContainer = range.startContainer;
 
-			rects.push( startContainerRect );
+			if ( isText( startContainer ) ) {
+				startContainer = startContainer.parentNode;
+			}
+
+			const rect = new Rect( startContainer.getBoundingClientRect() );
+			rect.right = rect.left;
+			rect.width = 0;
+
+			rects.push( rect );
 		}
 
 		return rects;

+ 1 - 1
packages/ckeditor5-utils/src/dom/remove.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 3 - 4
packages/ckeditor5-utils/src/dom/scroll.js

@@ -1,16 +1,15 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
-/* global Node */
-
 /**
  * @module utils/dom/scroll
  */
 
 import isRange from './isrange';
 import Rect from './rect';
+import isText from './istext';
 
 const utils = {};
 
@@ -255,7 +254,7 @@ function getParentElement( elementOrRange ) {
 		let parent = elementOrRange.commonAncestorContainer;
 
 		// If a Range is attached to the Text, use the closest element ancestor.
-		if ( parent.nodeType == Node.TEXT_NODE ) {
+		if ( isText( parent ) ) {
 			parent = parent.parentNode;
 		}
 

+ 1 - 1
packages/ckeditor5-utils/src/dom/setdatainelement.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/dom/tounit.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/elementreplacer.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 201 - 153
packages/ckeditor5-utils/src/emittermixin.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -15,81 +15,21 @@ const _listeningTo = Symbol( 'listeningTo' );
 const _emitterId = Symbol( 'emitterId' );
 
 /**
- * Mixin that injects the events API into its host.
+ * Mixin that injects the {@link ~Emitter events API} into its host.
  *
  * @mixin EmitterMixin
  * @implements module:utils/emittermixin~Emitter
  */
 const EmitterMixin = {
 	/**
-	 * Registers a callback function to be executed when an event is fired.
-	 *
-	 * Events can be grouped in namespaces using `:`.
-	 * When namespaced event is fired, it additionally fires all callbacks for that namespace.
-	 *
-	 *		myEmitter.on( 'myGroup', genericCallback );
-	 *		myEmitter.on( 'myGroup:myEvent', specificCallback );
-	 *
-	 *		// genericCallback is fired.
-	 *		myEmitter.fire( 'myGroup' );
-	 *		// both genericCallback and specificCallback are fired.
-	 *		myEmitter.fire( 'myGroup:myEvent' );
-	 *		// genericCallback is fired even though there are no callbacks for "foo".
-	 *		myEmitter.fire( 'myGroup:foo' );
-	 *
-	 * An event callback can {@link module:utils/eventinfo~EventInfo#stop stop the event} and
-	 * set the {@link module:utils/eventinfo~EventInfo#return return value} of the {@link #fire} method.
-	 *
-	 * @method #on
-	 * @param {String} event The name of the event.
-	 * @param {Function} callback The function to be called on event.
-	 * @param {Object} [options={}] Additional options.
-	 * @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
-	 * the priority value the sooner the callback will be fired. Events having the same priority are called in the
-	 * order they were added.
+	 * @inheritDoc
 	 */
 	on( event, callback, options = {} ) {
-		createEventNamespace( this, event );
-		const lists = getCallbacksListsForNamespace( this, event );
-		const priority = priorities.get( options.priority );
-
-		callback = {
-			callback,
-			priority
-		};
-
-		// Add the callback to all callbacks list.
-		for ( const callbacks of lists ) {
-			// Add the callback to the list in the right priority position.
-			let added = false;
-
-			for ( let i = 0; i < callbacks.length; i++ ) {
-				if ( callbacks[ i ].priority < priority ) {
-					callbacks.splice( i, 0, callback );
-					added = true;
-
-					break;
-				}
-			}
-
-			// Add at the end, if right place was not found.
-			if ( !added ) {
-				callbacks.push( callback );
-			}
-		}
+		this.listenTo( this, event, callback, options );
 	},
 
 	/**
-	 * Registers a callback function to be executed on the next time the event is fired only. This is similar to
-	 * calling {@link #on} followed by {@link #off} in the callback.
-	 *
-	 * @method #once
-	 * @param {String} event The name of the event.
-	 * @param {Function} callback The function to be called on event.
-	 * @param {Object} [options={}] Additional options.
-	 * @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
-	 * the priority value the sooner the callback will be fired. Events having the same priority are called in the
-	 * order they were added.
+	 * @inheritDoc
 	 */
 	once( event, callback, options ) {
 		const onceCallback = function( event, ...args ) {
@@ -101,43 +41,20 @@ const EmitterMixin = {
 		};
 
 		// Make a similar on() call, simply replacing the callback.
-		this.on( event, onceCallback, options );
+		this.listenTo( this, event, onceCallback, options );
 	},
 
 	/**
-	 * Stops executing the callback on the given event.
-	 *
-	 * @method #off
-	 * @param {String} event The name of the event.
-	 * @param {Function} callback The function to stop being called.
+	 * @inheritDoc
 	 */
 	off( event, callback ) {
-		const lists = getCallbacksListsForNamespace( this, event );
-
-		for ( const callbacks of lists ) {
-			for ( let i = 0; i < callbacks.length; i++ ) {
-				if ( callbacks[ i ].callback == callback ) {
-					// Remove the callback from the list (fixing the next index).
-					callbacks.splice( i, 1 );
-					i--;
-				}
-			}
-		}
+		this.stopListening( this, event, callback );
 	},
 
 	/**
-	 * Registers a callback function to be executed when an event is fired in a specific (emitter) object.
-	 *
-	 * @method #listenTo
-	 * @param {module:utils/emittermixin~Emitter} emitter The object that fires the event.
-	 * @param {String} event The name of the event.
-	 * @param {Function} callback The function to be called on event.
-	 * @param {Object} [options={}] Additional options.
-	 * @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
-	 * the priority value the sooner the callback will be fired. Events having the same priority are called in the
-	 * order they were added.
+	 * @inheritDoc
 	 */
-	listenTo( emitter, event, callback, options ) {
+	listenTo( emitter, event, callback, options = {} ) {
 		let emitterInfo, eventCallbacks;
 
 		// _listeningTo contains a list of emitters that this object is listening to.
@@ -180,23 +97,38 @@ const EmitterMixin = {
 		eventCallbacks.push( callback );
 
 		// Finally register the callback to the event.
-		emitter.on( event, callback, options );
+		createEventNamespace( emitter, event );
+		const lists = getCallbacksListsForNamespace( emitter, event );
+		const priority = priorities.get( options.priority );
+
+		const callbackDefinition = {
+			callback,
+			priority
+		};
+
+		// Add the callback to all callbacks list.
+		for ( const callbacks of lists ) {
+			// Add the callback to the list in the right priority position.
+			let added = false;
+
+			for ( let i = 0; i < callbacks.length; i++ ) {
+				if ( callbacks[ i ].priority < priority ) {
+					callbacks.splice( i, 0, callbackDefinition );
+					added = true;
+
+					break;
+				}
+			}
+
+			// Add at the end, if right place was not found.
+			if ( !added ) {
+				callbacks.push( callbackDefinition );
+			}
+		}
 	},
 
 	/**
-	 * Stops listening for events. It can be used at different levels:
-	 *
-	 * * To stop listening to a specific callback.
-	 * * To stop listening to a specific event.
-	 * * To stop listening to all events fired by a specific object.
-	 * * To stop listening to all events fired by all object.
-	 *
-	 * @method #stopListening
-	 * @param {module:utils/emittermixin~Emitter} [emitter] The object to stop listening to. If omitted, stops it for all objects.
-	 * @param {String} [event] (Requires the `emitter`) The name of the event to stop listening to. If omitted, stops it
-	 * for all events from `emitter`.
-	 * @param {Function} [callback] (Requires the `event`) The function to be removed from the call list for the given
-	 * `event`.
+	 * @inheritDoc
 	 */
 	stopListening( emitter, event, callback ) {
 		const emitters = this[ _listeningTo ];
@@ -211,13 +143,14 @@ const EmitterMixin = {
 
 		// All params provided. off() that single callback.
 		if ( callback ) {
-			emitter.off( event, callback );
+			removeCallback( emitter, event, callback );
 		}
 		// Only `emitter` and `event` provided. off() all callbacks for that event.
 		else if ( eventCallbacks ) {
 			while ( ( callback = eventCallbacks.pop() ) ) {
-				emitter.off( event, callback );
+				removeCallback( emitter, event, callback );
 			}
+
 			delete emitterInfo.callbacks[ event ];
 		}
 		// Only `emitter` provided. off() all events for that emitter.
@@ -237,17 +170,7 @@ const EmitterMixin = {
 	},
 
 	/**
-	 * Fires an event, executing all callbacks registered for it.
-	 *
-	 * The first parameter passed to callbacks is an {@link module:utils/eventinfo~EventInfo} object,
-	 * followed by the optional `args` provided in the `fire()` method call.
-	 *
-	 * @method #fire
-	 * @param {String|module:utils/eventinfo~EventInfo} eventOrInfo The name of the event or `EventInfo` object if event is delegated.
-	 * @param {...*} [args] Additional arguments to be passed to the callbacks.
-	 * @returns {*} By default the method returns `undefined`. However, the return value can be changed by listeners
-	 * through modification of the {@link module:utils/eventinfo~EventInfo#return}'s value (the event info
-	 * is the first param of every callback).
+	 * @inheritDoc
 	 */
 	fire( eventOrInfo, ...args ) {
 		const eventInfo = eventOrInfo instanceof EventInfo ? eventOrInfo : new EventInfo( this, eventOrInfo );
@@ -277,7 +200,7 @@ const EmitterMixin = {
 					// Remove the called mark for the next calls.
 					delete eventInfo.off.called;
 
-					this.off( event, callbacks[ i ].callback );
+					removeCallback( this, event, callbacks[ i ].callback );
 				}
 
 				// Do not execute next callbacks if stop() was called.
@@ -305,22 +228,7 @@ const EmitterMixin = {
 	},
 
 	/**
-	 * Delegates selected events to another {@link module:utils/emittermixin~Emitter}. For instance:
-	 *
-	 *		emitterA.delegate( 'eventX' ).to( emitterB );
-	 *		emitterA.delegate( 'eventX', 'eventY' ).to( emitterC );
-	 *
-	 * then `eventX` is delegated (fired by) `emitterB` and `emitterC` along with `data`:
-	 *
-	 *		emitterA.fire( 'eventX', data );
-	 *
-	 * and `eventY` is delegated (fired by) `emitterC` along with `data`:
-	 *
-	 *		emitterA.fire( 'eventY', data );
-	 *
-	 * @method #delegate
-	 * @param {...String} events Event names that will be delegated to another emitter.
-	 * @returns {module:utils/emittermixin~EmitterMixinDelegateChain}
+	 * @inheritDoc
 	 */
 	delegate( ...events ) {
 		return {
@@ -343,16 +251,7 @@ const EmitterMixin = {
 	},
 
 	/**
-	 * Stops delegating events. It can be used at different levels:
-	 *
-	 * * To stop delegating all events.
-	 * * To stop delegating a specific event to all emitters.
-	 * * To stop delegating a specific event to a specific emitter.
-	 *
-	 * @method #stopDelegating
-	 * @param {String} [event] The name of the event to stop delegating. If omitted, stops it all delegations.
-	 * @param {module:utils/emittermixin~Emitter} [emitter] (requires `event`) The object to stop delegating a particular event to.
-	 * If omitted, stops delegation of `event` to all emitters.
+	 * @inheritDoc
 	 */
 	stopDelegating( event, emitter ) {
 		if ( !this._delegations ) {
@@ -375,6 +274,143 @@ const EmitterMixin = {
 
 export default EmitterMixin;
 
+/**
+ * Emitter/listener interface.
+ *
+ * Can be easily implemented by a class by mixing the {@link module:utils/emittermixin~EmitterMixin} mixin.
+ *
+ * @interface Emitter
+ */
+
+/**
+ * Registers a callback function to be executed when an event is fired.
+ *
+ * Shorthand for {@link #listenTo `this.listenTo( this, event, callback, options )`} (it makes the emitter
+ * listen on itself).
+ *
+ * @method #on
+ * @param {String} event The name of the event.
+ * @param {Function} callback The function to be called on event.
+ * @param {Object} [options={}] Additional options.
+ * @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
+ * the priority value the sooner the callback will be fired. Events having the same priority are called in the
+ * order they were added.
+ */
+
+/**
+ * Registers a callback function to be executed on the next time the event is fired only. This is similar to
+ * calling {@link #on} followed by {@link #off} in the callback.
+ *
+ * @method #once
+ * @param {String} event The name of the event.
+ * @param {Function} callback The function to be called on event.
+ * @param {Object} [options={}] Additional options.
+ * @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
+ * the priority value the sooner the callback will be fired. Events having the same priority are called in the
+ * order they were added.
+ */
+
+/**
+ * Stops executing the callback on the given event.
+ * Shorthand for {@link #stopListening `this.stopListening( this, event, callback )`}.
+ *
+ * @method #off
+ * @param {String} event The name of the event.
+ * @param {Function} callback The function to stop being called.
+ */
+
+/**
+ * Registers a callback function to be executed when an event is fired in a specific (emitter) object.
+ *
+ * Events can be grouped in namespaces using `:`.
+ * When namespaced event is fired, it additionally fires all callbacks for that namespace.
+ *
+ *		// myEmitter.on( ... ) is a shorthand for myEmitter.listenTo( myEmitter, ... ).
+ *		myEmitter.on( 'myGroup', genericCallback );
+ *		myEmitter.on( 'myGroup:myEvent', specificCallback );
+ *
+ *		// genericCallback is fired.
+ *		myEmitter.fire( 'myGroup' );
+ *		// both genericCallback and specificCallback are fired.
+ *		myEmitter.fire( 'myGroup:myEvent' );
+ *		// genericCallback is fired even though there are no callbacks for "foo".
+ *		myEmitter.fire( 'myGroup:foo' );
+ *
+ * An event callback can {@link module:utils/eventinfo~EventInfo#stop stop the event} and
+ * set the {@link module:utils/eventinfo~EventInfo#return return value} of the {@link #fire} method.
+ *
+ * @method #listenTo
+ * @param {module:utils/emittermixin~Emitter} emitter The object that fires the event.
+ * @param {String} event The name of the event.
+ * @param {Function} callback The function to be called on event.
+ * @param {Object} [options={}] Additional options.
+ * @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
+ * the priority value the sooner the callback will be fired. Events having the same priority are called in the
+ * order they were added.
+ */
+
+/**
+ * Stops listening for events. It can be used at different levels:
+ *
+ * * To stop listening to a specific callback.
+ * * To stop listening to a specific event.
+ * * To stop listening to all events fired by a specific object.
+ * * To stop listening to all events fired by all objects.
+ *
+ * @method #stopListening
+ * @param {module:utils/emittermixin~Emitter} [emitter] The object to stop listening to. If omitted, stops it for all objects.
+ * @param {String} [event] (Requires the `emitter`) The name of the event to stop listening to. If omitted, stops it
+ * for all events from `emitter`.
+ * @param {Function} [callback] (Requires the `event`) The function to be removed from the call list for the given
+ * `event`.
+ */
+
+/**
+ * Fires an event, executing all callbacks registered for it.
+ *
+ * The first parameter passed to callbacks is an {@link module:utils/eventinfo~EventInfo} object,
+ * followed by the optional `args` provided in the `fire()` method call.
+ *
+ * @method #fire
+ * @param {String|module:utils/eventinfo~EventInfo} eventOrInfo The name of the event or `EventInfo` object if event is delegated.
+ * @param {...*} [args] Additional arguments to be passed to the callbacks.
+ * @returns {*} By default the method returns `undefined`. However, the return value can be changed by listeners
+ * through modification of the {@link module:utils/eventinfo~EventInfo#return `evt.return`}'s property (the event info
+ * is the first param of every callback).
+ */
+
+/**
+ * Delegates selected events to another {@link module:utils/emittermixin~Emitter}. For instance:
+ *
+ *		emitterA.delegate( 'eventX' ).to( emitterB );
+ *		emitterA.delegate( 'eventX', 'eventY' ).to( emitterC );
+ *
+ * then `eventX` is delegated (fired by) `emitterB` and `emitterC` along with `data`:
+ *
+ *		emitterA.fire( 'eventX', data );
+ *
+ * and `eventY` is delegated (fired by) `emitterC` along with `data`:
+ *
+ *		emitterA.fire( 'eventY', data );
+ *
+ * @method #delegate
+ * @param {...String} events Event names that will be delegated to another emitter.
+ * @returns {module:utils/emittermixin~EmitterMixinDelegateChain}
+ */
+
+/**
+ * Stops delegating events. It can be used at different levels:
+ *
+ * * To stop delegating all events.
+ * * To stop delegating a specific event to all emitters.
+ * * To stop delegating a specific event to a specific emitter.
+ *
+ * @method #stopDelegating
+ * @param {String} [event] The name of the event to stop delegating. If omitted, stops it all delegations.
+ * @param {module:utils/emittermixin~Emitter} [emitter] (requires `event`) The object to stop delegating a particular event to.
+ * If omitted, stops delegation of `event` to all emitters.
+ */
+
 /**
  * Checks if `listeningEmitter` listens to an emitter with given `listenedToEmitterId` and if so, returns that emitter.
  * If not, returns `null`.
@@ -508,7 +544,6 @@ function createEventNamespace( source, eventName ) {
 // Gets an array containing callbacks list for a given event and it's more specific events.
 // I.e. if given event is foo:bar and there is also foo:bar:abc event registered, this will
 // return callback list of foo:bar and foo:bar:abc (but not foo).
-// Returns empty array if given event has not been yet registered.
 function getCallbacksListsForNamespace( source, eventName ) {
 	const eventNode = getEvents( source )[ eventName ];
 
@@ -570,11 +605,24 @@ function fireDelegatedEvents( destinations, eventInfo, fireArgs ) {
 	}
 }
 
-/**
- * Interface representing classes which mix in {@link module:utils/emittermixin~EmitterMixin}.
- *
- * @interface Emitter
- */
+// Removes callback from emitter for given event.
+//
+// @param {module:utils/emittermixin~Emitter} emitter
+// @param {String} event
+// @param {Function} callback
+function removeCallback( emitter, event, callback ) {
+	const lists = getCallbacksListsForNamespace( emitter, event );
+
+	for ( const callbacks of lists ) {
+		for ( let i = 0; i < callbacks.length; i++ ) {
+			if ( callbacks[ i ].callback == callback ) {
+				// Remove the callback from the list (fixing the next index).
+				callbacks.splice( i, 1 );
+				i--;
+			}
+		}
+	}
+}
 
 /**
  * The return value of {@link ~EmitterMixin#delegate}.

+ 3 - 3
packages/ckeditor5-utils/src/env.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -21,9 +21,9 @@ const env = {
 	 * Indicates that application is running on Macintosh.
 	 *
 	 * @static
-	 * @member {Boolean} module:utils/env~env#mac
+	 * @member {Boolean} module:utils/env~env#isMac
 	 */
-	mac: isMac( userAgent )
+	isMac: isMac( userAgent )
 };
 
 export default env;

+ 1 - 1
packages/ckeditor5-utils/src/eventinfo.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/first.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/focustracker.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/isiterable.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 27 - 8
packages/ckeditor5-utils/src/keyboard.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -12,6 +12,18 @@
 import CKEditorError from './ckeditorerror';
 import env from './env';
 
+const macGlyphsToModifiers = {
+	'⌘': 'ctrl',
+	'⇧': 'shift',
+	'⌥': 'alt'
+};
+
+const modifiersToMacGlyphs = {
+	'ctrl': '⌘',
+	'shift': '⇧',
+	'alt': '⌥'
+};
+
 /**
  * Object with `keyName => keyCode` pairs for a set of known keys.
  *
@@ -96,15 +108,22 @@ export function parseKeystroke( keystroke ) {
  * @returns {String} Keystroke text specific for the environment.
  */
 export function getEnvKeystrokeText( keystroke ) {
-	const split = splitKeystrokeText( keystroke );
-
-	if ( env.mac ) {
-		if ( split[ 0 ].toLowerCase() == 'ctrl' ) {
-			return '⌘' + ( split[ 1 ] || '' );
-		}
+	if ( !env.isMac ) {
+		return keystroke;
 	}
 
-	return keystroke;
+	return splitKeystrokeText( keystroke )
+		// Replace modifiers (e.g. "ctrl") with Mac glyphs (e.g. "⌘") first.
+		.map( key => modifiersToMacGlyphs[ key.toLowerCase() ] || key )
+
+		// Decide whether to put "+" between keys in the keystroke or not.
+		.reduce( ( value, key ) => {
+			if ( value.slice( -1 ) in macGlyphsToModifiers ) {
+				return value + key;
+			} else {
+				return value + '+' + key;
+			}
+		} );
 }
 
 function generateKnownKeyCodes() {

+ 1 - 1
packages/ckeditor5-utils/src/keystrokehandler.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 7 - 7
packages/ckeditor5-utils/src/locale.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -16,20 +16,20 @@ export default class Locale {
 	/**
 	 * Creates a new instance of the Locale class.
 	 *
-	 * @param {String} [lang='en'] The language code in [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) format.
+	 * @param {String} [language='en'] The language code in [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) format.
 	 */
-	constructor( lang ) {
+	constructor( language ) {
 		/**
 		 * The language code in [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) format.
 		 *
 		 * @readonly
 		 * @member {String}
 		 */
-		this.lang = lang || 'en';
+		this.language = language || 'en';
 
 		/**
-		 * Translates the given string to the {@link #lang}. This method is also availble in {@link module:core/editor/editor~Editor#t} and
-		 * {@link module:ui/view~View#t}.
+		 * Translates the given string to the {@link #language}. This method is also available in {@link module:core/editor/editor~Editor#t}
+		 * and {@link module:ui/view~View#t}.
 		 *
 		 * The strings may contain placeholders (`%<index>`) for values which are passed as the second argument.
 		 * `<index>` is the index in the `values` array.
@@ -55,7 +55,7 @@ export default class Locale {
 	 * @private
 	 */
 	_t( str, values ) {
-		let translatedString = translate( this.lang, str );
+		let translatedString = translate( this.language, str );
 
 		if ( values ) {
 			translatedString = translatedString.replace( /%(\d+)/g, ( match, index ) => {

+ 1 - 1
packages/ckeditor5-utils/src/log.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/mapsequal.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/mix.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/nth.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/objecttomap.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 317 - 267
packages/ckeditor5-utils/src/observablemixin.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -12,13 +12,13 @@ import CKEditorError from './ckeditorerror';
 import extend from './lib/lodash/extend';
 import isObject from './lib/lodash/isObject';
 
-const attributesSymbol = Symbol( 'attributes' );
+const observablePropertiesSymbol = Symbol( 'observableProperties' );
 const boundObservablesSymbol = Symbol( 'boundObservables' );
-const boundAttributesSymbol = Symbol( 'boundAttributes' );
+const boundPropertiesSymbol = Symbol( 'boundProperties' );
 
 /**
- * Mixin that injects the "observable attributes" and data binding functionality.
- * Used mainly in the {@link module:ui/model~Model} class.
+ * Mixin that injects the "observable properties" and data binding functionality described in the
+ * {@link ~Observable} interface.
  *
  * @mixin ObservableMixin
  * @mixes module:utils/emittermixin~EmitterMixin
@@ -26,24 +26,13 @@ const boundAttributesSymbol = Symbol( 'boundAttributes' );
  */
 const ObservableMixin = {
 	/**
-	 * Creates and sets the value of an observable attribute of this object. Such an attribute becomes a part
-	 * of the state and is be observable.
-	 *
-	 * It accepts also a single object literal containing key/value pairs with attributes to be set.
-	 *
-	 * This method throws the observable-set-cannot-override error if the observable instance already
-	 * have a property with a given attribute name. This prevents from mistakenly overriding existing
-	 * properties and methods, but means that `foo.set( 'bar', 1 )` may be slightly slower than `foo.bar = 1`.
-	 *
-	 * @method #set
-	 * @param {String} name The attributes name.
-	 * @param {*} value The attributes value.
+	 * @inheritDoc
 	 */
 	set( name, value ) {
 		// If the first parameter is an Object, iterate over its properties.
 		if ( isObject( name ) ) {
-			Object.keys( name ).forEach( attr => {
-				this.set( attr, name[ attr ] );
+			Object.keys( name ).forEach( property => {
+				this.set( property, name[ property ] );
 			}, this );
 
 			return;
@@ -51,21 +40,21 @@ const ObservableMixin = {
 
 		initObservable( this );
 
-		const attributes = this[ attributesSymbol ];
+		const properties = this[ observablePropertiesSymbol ];
 
-		if ( ( name in this ) && !attributes.has( name ) ) {
+		if ( ( name in this ) && !properties.has( name ) ) {
 			/**
 			 * Cannot override an existing property.
 			 *
-			 * This error is thrown when trying to {@link ~Observable#set set} an attribute with
+			 * This error is thrown when trying to {@link ~Observable#set set} an property with
 			 * a name of an already existing property. For example:
 			 *
 			 *		let observable = new Model();
 			 *		observable.property = 1;
-			 *		observable.set( 'property', 2 );		// throws
+			 *		observable.set( 'property', 2 );			// throws
 			 *
-			 *		observable.set( 'attr', 1 );
-			 *		observable.set( 'attr', 2 );			// ok, because this is an existing attribute.
+			 *		observable.set( 'property', 1 );
+			 *		observable.set( 'property', 2 );			// ok, because this is an existing property.
 			 *
 			 * @error observable-set-cannot-override
 			 */
@@ -77,16 +66,16 @@ const ObservableMixin = {
 			configurable: true,
 
 			get() {
-				return attributes.get( name );
+				return properties.get( name );
 			},
 
 			set( value ) {
-				const oldValue = attributes.get( name );
+				const oldValue = properties.get( name );
 
 				// Allow undefined as an initial value like A.define( 'x', undefined ) (#132).
-				// Note: When attributes map has no such own property, then its value is undefined.
-				if ( oldValue !== value || !attributes.has( name ) ) {
-					attributes.set( name, value );
+				// Note: When properties map has no such own property, then its value is undefined.
+				if ( oldValue !== value || !properties.has( name ) ) {
+					properties.set( name, value );
 					this.fire( 'change:' + name, name, value, oldValue );
 				}
 			}
@@ -96,148 +85,127 @@ const ObservableMixin = {
 	},
 
 	/**
-	 * Binds observable attributes to another objects implementing {@link ~ObservableMixin}
-	 * interface (like {@link module:ui/model~Model}).
-	 *
-	 * Once bound, the observable will immediately share the current state of attributes
-	 * of the observable it is bound to and react to the changes to these attributes
-	 * in the future.
-	 *
-	 * **Note**: To release the binding use {@link module:utils/observablemixin~ObservableMixin#unbind}.
-	 *
-	 *		A.bind( 'a' ).to( B );
-	 *		A.bind( 'a' ).to( B, 'b' );
-	 *		A.bind( 'a', 'b' ).to( B, 'c', 'd' );
-	 *		A.bind( 'a' ).to( B, 'b', C, 'd', ( b, d ) => b + d );
-	 *
-	 * @method #bind
-	 * @param {...String} bindAttrs Observable attributes that will be bound to another observable(s).
-	 * @returns {module:utils/observablemixin~BindChain}
+	 * @inheritDoc
 	 */
-	bind( ...bindAttrs ) {
-		if ( !bindAttrs.length || !isStringArray( bindAttrs ) ) {
+	bind( ...bindProperties ) {
+		if ( !bindProperties.length || !isStringArray( bindProperties ) ) {
 			/**
-			 * All attributes must be strings.
+			 * All properties must be strings.
 			 *
-			 * @error observable-bind-wrong-attrs
+			 * @error observable-bind-wrong-properties
 			 */
-			throw new CKEditorError( 'observable-bind-wrong-attrs: All attributes must be strings.' );
+			throw new CKEditorError( 'observable-bind-wrong-properties: All properties must be strings.' );
 		}
 
-		if ( ( new Set( bindAttrs ) ).size !== bindAttrs.length ) {
+		if ( ( new Set( bindProperties ) ).size !== bindProperties.length ) {
 			/**
-			 * Attributes must be unique.
+			 * Properties must be unique.
 			 *
-			 * @error observable-bind-duplicate-attrs
+			 * @error observable-bind-duplicate-properties
 			 */
-			throw new CKEditorError( 'observable-bind-duplicate-attrs: Attributes must be unique.' );
+			throw new CKEditorError( 'observable-bind-duplicate-properties: Properties must be unique.' );
 		}
 
 		initObservable( this );
 
-		const boundAttributes = this[ boundAttributesSymbol ];
+		const boundProperties = this[ boundPropertiesSymbol ];
 
-		bindAttrs.forEach( attrName => {
-			if ( boundAttributes.has( attrName ) ) {
+		bindProperties.forEach( propertyName => {
+			if ( boundProperties.has( propertyName ) ) {
 				/**
-				 * Cannot bind the same attribute more that once.
+				 * Cannot bind the same property more that once.
 				 *
 				 * @error observable-bind-rebind
 				 */
-				throw new CKEditorError( 'observable-bind-rebind: Cannot bind the same attribute more that once.' );
+				throw new CKEditorError( 'observable-bind-rebind: Cannot bind the same property more that once.' );
 			}
 		} );
 
 		const bindings = new Map();
 
-		/**
-		 * @typedef Binding
-		 * @type Object
-		 * @property {Array} attr Attribute which is bound.
-		 * @property {Array} to Array of observable–attribute components of the binding (`{ observable: ..., attr: .. }`).
-		 * @property {Array} callback A function which processes `to` components.
-		 */
-		bindAttrs.forEach( a => {
-			const binding = { attr: a, to: [] };
+		// @typedef {Object} Binding
+		// @property {Array} property Property which is bound.
+		// @property {Array} to Array of observable–property components of the binding (`{ observable: ..., property: .. }`).
+		// @property {Array} callback A function which processes `to` components.
+		bindProperties.forEach( a => {
+			const binding = { property: a, to: [] };
 
-			boundAttributes.set( a, binding );
+			boundProperties.set( a, binding );
 			bindings.set( a, binding );
 		} );
 
-		/**
-		 * @typedef BindChain
-		 * @type Object
-		 * @property {Function} to See {@link ~ObservableMixin#_bindTo}.
-		 * @property {module:utils/observablemixin~Observable} _observable The observable which initializes the binding.
-		 * @property {Array} _bindAttrs Array of `_observable` attributes to be bound.
-		 * @property {Array} _to Array of `to()` observable–attributes (`{ observable: toObservable, attrs: ...toAttrs }`).
-		 * @property {Map} _bindings Stores bindings to be kept in
-		 *  {@link ~ObservableMixin#_boundAttributes}/{@link ~ObservableMixin#_boundObservables}
-		 * initiated in this binding chain.
-		 */
+		// @typedef {Object} BindChain
+		// @property {Function} to See {@link ~ObservableMixin#_bindTo}.
+		// @property {Function} toMany See {@link ~ObservableMixin#_bindToMany}.
+		// @property {module:utils/observablemixin~Observable} _observable The observable which initializes the binding.
+		// @property {Array} _bindProperties Array of `_observable` properties to be bound.
+		// @property {Array} _to Array of `to()` observable–properties (`{ observable: toObservable, properties: ...toProperties }`).
+		// @property {Map} _bindings Stores bindings to be kept in
+		// {@link ~ObservableMixin#_boundProperties}/{@link ~ObservableMixin#_boundObservables}
+		// initiated in this binding chain.
 		return {
 			to: bindTo,
+			toMany: bindToMany,
 
 			_observable: this,
-			_bindAttrs: bindAttrs,
+			_bindProperties: bindProperties,
 			_to: [],
 			_bindings: bindings
 		};
 	},
 
 	/**
-	 * Removes the binding created with {@link ~ObservableMixin#bind}.
-	 *
-	 *		A.unbind( 'a' );
-	 *		A.unbind();
-	 *
-	 * @method #unbind
-	 * @param {...String} [unbindAttrs] Observable attributes to be unbound. All the bindings will
-	 * be released if no attributes provided.
+	 * @inheritDoc
 	 */
-	unbind( ...unbindAttrs ) {
+	unbind( ...unbindProperties ) {
 		// Nothing to do here if not inited yet.
-		if ( !( attributesSymbol in this ) ) {
+		if ( !( observablePropertiesSymbol in this ) ) {
 			return;
 		}
 
-		const boundAttributes = this[ boundAttributesSymbol ];
+		const boundProperties = this[ boundPropertiesSymbol ];
 		const boundObservables = this[ boundObservablesSymbol ];
 
-		if ( unbindAttrs.length ) {
-			if ( !isStringArray( unbindAttrs ) ) {
+		if ( unbindProperties.length ) {
+			if ( !isStringArray( unbindProperties ) ) {
 				/**
-				 * Attributes must be strings.
+				 * Properties must be strings.
 				 *
-				 * @error observable-unbind-wrong-attrs
+				 * @error observable-unbind-wrong-properties
 				 */
-				throw new CKEditorError( 'observable-unbind-wrong-attrs: Attributes must be strings.' );
+				throw new CKEditorError( 'observable-unbind-wrong-properties: Properties must be strings.' );
 			}
 
-			unbindAttrs.forEach( attrName => {
-				const binding = boundAttributes.get( attrName );
-				let toObservable, toAttr, toAttrs, toAttrBindings;
+			unbindProperties.forEach( propertyName => {
+				const binding = boundProperties.get( propertyName );
+
+				// Nothing to do if the binding is not defined
+				if ( !binding ) {
+					return;
+				}
+
+				let toObservable, toProperty, toProperties, toPropertyBindings;
 
 				binding.to.forEach( to => {
 					// TODO: ES6 destructuring.
 					toObservable = to[ 0 ];
-					toAttr = to[ 1 ];
-					toAttrs = boundObservables.get( toObservable );
-					toAttrBindings = toAttrs[ toAttr ];
+					toProperty = to[ 1 ];
+					toProperties = boundObservables.get( toObservable );
+					toPropertyBindings = toProperties[ toProperty ];
 
-					toAttrBindings.delete( binding );
+					toPropertyBindings.delete( binding );
 
-					if ( !toAttrBindings.size ) {
-						delete toAttrs[ toAttr ];
+					if ( !toPropertyBindings.size ) {
+						delete toProperties[ toProperty ];
 					}
 
-					if ( !Object.keys( toAttrs ).length ) {
+					if ( !Object.keys( toProperties ).length ) {
 						boundObservables.delete( toObservable );
 						this.stopListening( toObservable, 'change' );
 					}
 				} );
 
-				boundAttributes.delete( attrName );
+				boundProperties.delete( propertyName );
 			} );
 		} else {
 			boundObservables.forEach( ( bindings, boundObservable ) => {
@@ -245,66 +213,12 @@ const ObservableMixin = {
 			} );
 
 			boundObservables.clear();
-			boundAttributes.clear();
+			boundProperties.clear();
 		}
 	},
 
 	/**
-	 * Turns the given methods of this object into event-based ones. This means that the new method will fire an event
-	 * (named after the method) and the original action will be plugged as a listener to that event.
-	 *
-	 * This is a very simplified method decoration. Itself it doesn't change the behavior of a method (expect adding the event),
-	 * but it allows to modify it later on by listening to the method's event.
-	 *
-	 * For example, in order to cancel the method execution one can stop the event:
-	 *
-	 *		class Foo {
-	 *			constructor() {
-	 *				this.decorate( 'method' );
-	 *			}
-	 *
-	 *			method() {
-	 *				console.log( 'called!' );
-	 *			}
-	 *		}
-	 *
-	 *		const foo = new Foo();
-	 *		foo.on( 'method', ( evt ) => {
-	 *			evt.stop();
-	 *		}, { priority: 'high' } );
-	 *
-	 *		foo.method(); // Nothing is logged.
-	 *
-	 *
-	 * Note: we used a high priority listener here to execute this callback before the one which
-	 * calls the orignal method (which used the default priority).
-	 *
-	 * It's also possible to change the return value:
-	 *
-	 *		foo.on( 'method', ( evt ) => {
-	 *			evt.return = 'Foo!';
-	 *		} );
-	 *
-	 *		foo.method(); // -> 'Foo'
-	 *
-	 * Finally, it's possible to access and modify the parameters:
-	 *
-	 *		method( a, b ) {
-	 *			console.log( `${ a }, ${ b }`  );
-	 *		}
-	 *
-	 *		// ...
-	 *
-	 *		foo.on( 'method', ( evt, args ) => {
-	 *			args[ 0 ] = 3;
-	 *
-	 *			console.log( args[ 1 ] ); // -> 2
-	 *		}, { priority: 'high' } );
-	 *
-	 *		foo.method( 1, 2 ); // -> '3, 2'
-	 *
-	 * @method #decorate
-	 * @param {String} methodName Name of the method to decorate.
+	 * @inheritDoc
 	 */
 	decorate( methodName ) {
 		const originalMethod = this[ methodName ];
@@ -331,23 +245,10 @@ const ObservableMixin = {
 			return this.fire( methodName, args );
 		};
 	}
-
-	/**
-	 * @private
-	 * @member ~ObservableMixin#_boundAttributes
-	 */
-
-	/**
-	 * @private
-	 * @member ~ObservableMixin#_boundObservables
-	 */
-
-	/**
-	 * @private
-	 * @member ~ObservableMixin#_bindTo
-	 */
 };
 
+extend( ObservableMixin, EmitterMixin );
+
 export default ObservableMixin;
 
 // Init symbol properties needed to for the observable mechanism to work.
@@ -356,7 +257,7 @@ export default ObservableMixin;
 // @param {module:utils/observablemixin~ObservableMixin} observable
 function initObservable( observable ) {
 	// Do nothing if already inited.
-	if ( attributesSymbol in observable ) {
+	if ( observablePropertiesSymbol in observable ) {
 		return;
 	}
 
@@ -364,13 +265,13 @@ function initObservable( observable ) {
 	//
 	// @private
 	// @type {Map}
-	Object.defineProperty( observable, attributesSymbol, {
+	Object.defineProperty( observable, observablePropertiesSymbol, {
 		value: new Map()
 	} );
 
 	// Map containing bindings to external observables. It shares the binding objects
-	// (`{ observable: A, attr: 'a', to: ... }`) with {@link module:utils/observablemixin~ObservableMixin#_boundAttributes} and
-	// it is used to observe external observables to update own attributes accordingly.
+	// (`{ observable: A, property: 'a', to: ... }`) with {@link module:utils/observablemixin~ObservableMixin#_boundProperties} and
+	// it is used to observe external observables to update own properties accordingly.
 	// See {@link module:utils/observablemixin~ObservableMixin#bind}.
 	//
 	//		A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
@@ -379,11 +280,11 @@ function initObservable( observable ) {
 	//			Map( {
 	//				B: {
 	//					x: Set( [
-	//						{ observable: A, attr: 'a', to: [ [ B, 'x' ] ] },
-	//						{ observable: A, attr: 'c', to: [ [ B, 'x' ] ] }
+	//						{ observable: A, property: 'a', to: [ [ B, 'x' ] ] },
+	//						{ observable: A, property: 'c', to: [ [ B, 'x' ] ] }
 	//					] ),
 	//					y: Set( [
-	//						{ observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
+	//						{ observable: A, property: 'b', to: [ [ B, 'y' ] ] },
 	//					] )
 	//				}
 	//			} )
@@ -394,19 +295,19 @@ function initObservable( observable ) {
 	//			Map( {
 	//				B: {
 	//					x: Set( [
-	//						{ observable: A, attr: 'a', to: [ [ B, 'x' ] ] },
-	//						{ observable: A, attr: 'c', to: [ [ B, 'x' ] ] }
+	//						{ observable: A, property: 'a', to: [ [ B, 'x' ] ] },
+	//						{ observable: A, property: 'c', to: [ [ B, 'x' ] ] }
 	//					] ),
 	//					y: Set( [
-	//						{ observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
+	//						{ observable: A, property: 'b', to: [ [ B, 'y' ] ] },
 	//					] ),
 	//					z: Set( [
-	//						{ observable: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
+	//						{ observable: A, property: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
 	//					] )
 	//				},
 	//				C: {
 	//					w: Set( [
-	//						{ observable: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
+	//						{ observable: A, property: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
 	//					] )
 	//				}
 	//			} )
@@ -417,35 +318,35 @@ function initObservable( observable ) {
 		value: new Map()
 	} );
 
-	// Object that stores which attributes of this observable are bound and how. It shares
-	// the binding objects (`{ observable: A, attr: 'a', to: ... }`) with {@link utils.ObservableMixin#_boundObservables}.
+	// Object that stores which properties of this observable are bound and how. It shares
+	// the binding objects (`{ observable: A, property: 'a', to: ... }`) with {@link utils.ObservableMixin#_boundObservables}.
 	// This data structure is a reverse of {@link utils.ObservableMixin#_boundObservables} and it is helpful for
 	// {@link utils.ObservableMixin#unbind}.
 	//
 	// See {@link utils.ObservableMixin#bind}.
 	//
 	//		A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
-	//		console.log( A._boundAttributes );
+	//		console.log( A._boundProperties );
 	//
 	//			Map( {
-	//				a: { observable: A, attr: 'a', to: [ [ B, 'x' ] ] },
-	//				b: { observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
-	//				c: { observable: A, attr: 'c', to: [ [ B, 'x' ] ] }
+	//				a: { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
+	//				b: { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
+	//				c: { observable: A, property: 'c', to: [ [ B, 'x' ] ] }
 	//			} )
 	//
 	//		A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
-	//		console.log( A._boundAttributes );
+	//		console.log( A._boundProperties );
 	//
 	//			Map( {
-	//				a: { observable: A, attr: 'a', to: [ [ B, 'x' ] ] },
-	//				b: { observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
-	//				c: { observable: A, attr: 'c', to: [ [ B, 'x' ] ] },
-	//				d: { observable: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
+	//				a: { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
+	//				b: { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
+	//				c: { observable: A, property: 'c', to: [ [ B, 'x' ] ] },
+	//				d: { observable: A, property: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
 	//			} )
 	//
 	// @private
 	// @type {Map}
-	Object.defineProperty( observable, boundAttributesSymbol, {
+	Object.defineProperty( observable, boundPropertiesSymbol, {
 		value: new Map()
 	} );
 }
@@ -472,28 +373,28 @@ function bindTo( ...args ) {
 	// Eliminate A.bind( 'x', 'y' ).to( B, callback )
 	if ( numberOfBindings > 1 && parsedArgs.callback ) {
 		/**
-		 * Cannot bind multiple attributes and use a callback in one binding.
+		 * Cannot bind multiple properties and use a callback in one binding.
 		 *
 		 * @error observable-bind-to-extra-callback
 		 */
-		throw new CKEditorError( 'observable-bind-to-extra-callback: Cannot bind multiple attributes and use a callback in one binding.' );
+		throw new CKEditorError( 'observable-bind-to-extra-callback: Cannot bind multiple properties and use a callback in one binding.' );
 	}
 
 	parsedArgs.to.forEach( to => {
 		// Eliminate A.bind( 'x', 'y' ).to( B, 'a' )
-		if ( to.attrs.length && to.attrs.length !== numberOfBindings ) {
+		if ( to.properties.length && to.properties.length !== numberOfBindings ) {
 			/**
-			 * The number of attributes must match.
+			 * The number of properties must match.
 			 *
-			 * @error observable-bind-to-attrs-length
+			 * @error observable-bind-to-properties-length
 			 */
-			throw new CKEditorError( 'observable-bind-to-attrs-length: The number of attributes must match.' );
+			throw new CKEditorError( 'observable-bind-to-properties-length: The number of properties must match.' );
 		}
 
-		// When no to.attrs specified, observing source attributes instead i.e.
+		// When no to.properties specified, observing source properties instead i.e.
 		// A.bind( 'x', 'y' ).to( B ) -> Observe B.x and B.y
-		if ( !to.attrs.length ) {
-			to.attrs = this._bindAttrs;
+		if ( !to.properties.length ) {
+			to.properties = this._bindProperties;
 		}
 	} );
 
@@ -506,15 +407,52 @@ function bindTo( ...args ) {
 
 	attachBindToListeners( this._observable, this._to );
 
-	// Update observable._boundAttributes and observable._boundObservables.
+	// Update observable._boundProperties and observable._boundObservables.
 	updateBindToBound( this );
 
-	// Set initial values of bound attributes.
-	this._bindAttrs.forEach( attrName => {
-		updateBoundObservableAttr( this._observable, attrName );
+	// Set initial values of bound properties.
+	this._bindProperties.forEach( propertyName => {
+		updateBoundObservableProperty( this._observable, propertyName );
 	} );
 }
 
+// Binds to an attribute in a set of iterable observables.
+//
+// @private
+// @param {Array.<Observable>} observables
+// @param {String} attribute
+// @param {Function} callback
+function bindToMany( observables, attribute, callback ) {
+	if ( this._bindings.size > 1 ) {
+		/**
+		 * Binding one attribute to many observables only possible with one attribute.
+		 *
+		 * @error observable-bind-to-many-not-one-binding
+		 */
+		throw new CKEditorError( 'observable-bind-to-many-not-one-binding: Cannot bind multiple properties with toMany().' );
+	}
+
+	this.to(
+		// Bind to #attribute of each observable...
+		...getBindingTargets( observables, attribute ),
+		// ...using given callback to parse attribute values.
+		callback
+	);
+}
+
+// Returns an array of binding components for
+// {@link Observable#bind} from a set of iterable observables.
+//
+// @param {Array.<Observable>} observables
+// @param {String} attribute
+// @returns {Array.<String|Observable>}
+function getBindingTargets( observables, attribute ) {
+	const observableAndAttributePairs = observables.map( observable => [ observable, attribute ] );
+
+	// Merge pairs to one-dimension array of observables and attributes.
+	return Array.prototype.concat.apply( [], observableAndAttributePairs );
+}
+
 // Check if all entries of the array are of `String` type.
 //
 // @private
@@ -533,8 +471,8 @@ function isStringArray( arr ) {
 //
 //		{
 //			to: [
-//				{ observable: B, attrs: [ 'a' ] },
-//				{ observable: C, attrs: [ 'b' ] },
+//				{ observable: B, properties: [ 'a' ] },
+//				{ observable: C, properties: [ 'b' ] },
 //			],
 //			callback: call
 // 		}
@@ -562,9 +500,9 @@ function parseBindToArgs( ...args ) {
 
 	args.forEach( a => {
 		if ( typeof a == 'string' ) {
-			lastObservable.attrs.push( a );
+			lastObservable.properties.push( a );
 		} else if ( typeof a == 'object' ) {
-			lastObservable = { observable: a, attrs: [] };
+			lastObservable = { observable: a, properties: [] };
 			parsed.to.push( lastObservable );
 		} else {
 			throw new CKEditorError( 'observable-bind-to-parse-error: Invalid argument syntax in `to()`.' );
@@ -579,25 +517,25 @@ function parseBindToArgs( ...args ) {
 // @private
 // @param {Binding} binding A binding to store in {@link Observable#_boundObservables}.
 // @param {Observable} toObservable A observable, which is a new component of `binding`.
-// @param {String} toAttrName A name of `toObservable`'s attribute, a new component of the `binding`.
-function updateBoundObservables( observable, binding, toObservable, toAttrName ) {
+// @param {String} toPropertyName A name of `toObservable`'s property, a new component of the `binding`.
+function updateBoundObservables( observable, binding, toObservable, toPropertyName ) {
 	const boundObservables = observable[ boundObservablesSymbol ];
 	const bindingsToObservable = boundObservables.get( toObservable );
 	const bindings = bindingsToObservable || {};
 
-	if ( !bindings[ toAttrName ] ) {
-		bindings[ toAttrName ] = new Set();
+	if ( !bindings[ toPropertyName ] ) {
+		bindings[ toPropertyName ] = new Set();
 	}
 
 	// Pass the binding to a corresponding Set in `observable._boundObservables`.
-	bindings[ toAttrName ].add( binding );
+	bindings[ toPropertyName ].add( binding );
 
 	if ( !bindingsToObservable ) {
 		boundObservables.set( toObservable, bindings );
 	}
 }
 
-// Synchronizes {@link Observable#_boundAttributes} and {@link Observable#_boundObservables}
+// Synchronizes {@link Observable#_boundProperties} and {@link Observable#_boundObservables}
 // with {@link BindChain}.
 //
 // Assuming the following binding being created
@@ -607,16 +545,16 @@ function updateBoundObservables( observable, binding, toObservable, toAttrName )
 // the following bindings were initialized by {@link Observable#bind} in {@link BindChain#_bindings}:
 //
 // 		{
-// 			a: { observable: A, attr: 'a', to: [] },
-// 			b: { observable: A, attr: 'b', to: [] },
+// 			a: { observable: A, property: 'a', to: [] },
+// 			b: { observable: A, property: 'b', to: [] },
 // 		}
 //
 // Iterate over all bindings in this chain and fill their `to` properties with
 // corresponding to( ... ) arguments (components of the binding), so
 //
 // 		{
-// 			a: { observable: A, attr: 'a', to: [ B, 'x' ] },
-// 			b: { observable: A, attr: 'b', to: [ B, 'y' ] },
+// 			a: { observable: A, property: 'a', to: [ B, 'x' ] },
+// 			b: { observable: A, property: 'b', to: [ B, 'y' ] },
 // 		}
 //
 // Then update the structure of {@link Observable#_boundObservables} with updated
@@ -625,10 +563,10 @@ function updateBoundObservables( observable, binding, toObservable, toAttrName )
 // 		Map( {
 // 			B: {
 // 				x: Set( [
-// 					{ observable: A, attr: 'a', to: [ [ B, 'x' ] ] }
+// 					{ observable: A, property: 'a', to: [ [ B, 'x' ] ] }
 // 				] ),
 // 				y: Set( [
-// 					{ observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
+// 					{ observable: A, property: 'b', to: [ [ B, 'y' ] ] },
 // 				] )
 //			}
 // 		} )
@@ -636,31 +574,31 @@ function updateBoundObservables( observable, binding, toObservable, toAttrName )
 // @private
 // @param {BindChain} chain The binding initialized by {@link Observable#bind}.
 function updateBindToBound( chain ) {
-	let toAttr;
+	let toProperty;
 
-	chain._bindings.forEach( ( binding, attrName ) => {
+	chain._bindings.forEach( ( binding, propertyName ) => {
 		// Note: For a binding without a callback, this will run only once
 		// like in A.bind( 'x', 'y' ).to( B, 'a', 'b' )
 		// TODO: ES6 destructuring.
 		chain._to.forEach( to => {
-			toAttr = to.attrs[ binding.callback ? 0 : chain._bindAttrs.indexOf( attrName ) ];
+			toProperty = to.properties[ binding.callback ? 0 : chain._bindProperties.indexOf( propertyName ) ];
 
-			binding.to.push( [ to.observable, toAttr ] );
-			updateBoundObservables( chain._observable, binding, to.observable, toAttr );
+			binding.to.push( [ to.observable, toProperty ] );
+			updateBoundObservables( chain._observable, binding, to.observable, toProperty );
 		} );
 	} );
 }
 
-// Updates an attribute of a {@link Observable} with a value
-// determined by an entry in {@link Observable#_boundAttributes}.
+// Updates an property of a {@link Observable} with a value
+// determined by an entry in {@link Observable#_boundProperties}.
 //
 // @private
-// @param {Observable} observable A observable which attribute is to be updated.
-// @param {String} attrName An attribute to be updated.
-function updateBoundObservableAttr( observable, attrName ) {
-	const boundAttributes = observable[ boundAttributesSymbol ];
-	const binding = boundAttributes.get( attrName );
-	let attrValue;
+// @param {Observable} observable A observable which property is to be updated.
+// @param {String} propertyName An property to be updated.
+function updateBoundObservableProperty( observable, propertyName ) {
+	const boundProperties = observable[ boundPropertiesSymbol ];
+	const binding = boundProperties.get( propertyName );
+	let propertyValue;
 
 	// When a binding with callback is created like
 	//
@@ -668,21 +606,21 @@ function updateBoundObservableAttr( observable, attrName ) {
 	//
 	// collect B.b and C.c, then pass them to callback to set A.a.
 	if ( binding.callback ) {
-		attrValue = binding.callback.apply( observable, binding.to.map( to => to[ 0 ][ to[ 1 ] ] ) );
+		propertyValue = binding.callback.apply( observable, binding.to.map( to => to[ 0 ][ to[ 1 ] ] ) );
 	} else {
-		attrValue = binding.to[ 0 ];
-		attrValue = attrValue[ 0 ][ attrValue[ 1 ] ];
+		propertyValue = binding.to[ 0 ];
+		propertyValue = propertyValue[ 0 ][ propertyValue[ 1 ] ];
 	}
 
-	if ( observable.hasOwnProperty( attrName ) ) {
-		observable[ attrName ] = attrValue;
+	if ( observable.hasOwnProperty( propertyName ) ) {
+		observable[ propertyName ] = propertyValue;
 	} else {
-		observable.set( attrName, attrValue );
+		observable.set( propertyName, propertyValue );
 	}
 }
 
 // Starts listening to changes in {@link BindChain._to} observables to update
-// {@link BindChain._observable} {@link BindChain._bindAttrs}. Also sets the
+// {@link BindChain._observable} {@link BindChain._bindProperties}. Also sets the
 // initial state of {@link BindChain._observable}.
 //
 // @private
@@ -695,14 +633,14 @@ function attachBindToListeners( observable, toBindings ) {
 		// If there's already a chain between the observables (`observable` listens to
 		// `to.observable`), there's no need to create another `change` event listener.
 		if ( !boundObservables.get( to.observable ) ) {
-			observable.listenTo( to.observable, 'change', ( evt, attrName ) => {
-				bindings = boundObservables.get( to.observable )[ attrName ];
+			observable.listenTo( to.observable, 'change', ( evt, propertyName ) => {
+				bindings = boundObservables.get( to.observable )[ propertyName ];
 
-				// Note: to.observable will fire for any attribute change, react
-				// to changes of attributes which are bound only.
+				// Note: to.observable will fire for any property change, react
+				// to changes of properties which are bound only.
 				if ( bindings ) {
 					bindings.forEach( binding => {
-						updateBoundObservableAttr( observable, binding.attr );
+						updateBoundObservableProperty( observable, binding.property );
 					} );
 				}
 			} );
@@ -710,10 +648,17 @@ function attachBindToListeners( observable, toBindings ) {
 	} );
 }
 
-extend( ObservableMixin, EmitterMixin );
+/**
+ * Interface which adds "observable properties" and data binding functionality.
+ *
+ * Can be easily implemented by a class by mixing the {@link module:utils/observablemixin~ObservableMixin} mixin.
+ *
+ * @interface Observable
+ * @extends module:utils/emittermixin~Emitter
+ */
 
 /**
- * Fired when an attribute changed value.
+ * Fired when a property changed value.
  *
  *		observable.set( 'prop', 1 );
  *
@@ -723,14 +668,119 @@ extend( ObservableMixin, EmitterMixin );
  *
  *		observable.prop = 2; // -> 'prop has changed from 1 to 2'
  *
- * @event module:utils/observablemixin~ObservableMixin#change:{attribute}
- * @param {String} name The attribute name.
- * @param {*} value The new attribute value.
- * @param {*} oldValue The previous attribute value.
+ * @event change:{property}
+ * @param {String} name The property name.
+ * @param {*} value The new property value.
+ * @param {*} oldValue The previous property value.
  */
 
 /**
- * Interface representing classes which mix in {@link module:utils/observablemixin~ObservableMixin}.
+ * Creates and sets the value of an observable property of this object. Such an property becomes a part
+ * of the state and is be observable.
  *
- * @interface Observable
+ * It accepts also a single object literal containing key/value pairs with properties to be set.
+ *
+ * This method throws the `observable-set-cannot-override` error if the observable instance already
+ * have a property with the given property name. This prevents from mistakenly overriding existing
+ * properties and methods, but means that `foo.set( 'bar', 1 )` may be slightly slower than `foo.bar = 1`.
+ *
+ * @method #set
+ * @param {String|Object} name The property's name or object with `name=>value` pairs.
+ * @param {*} [value] The property's value (if `name` was passed in the first parameter).
+ */
+
+/**
+ * Binds observable properties to another objects implementing {@link module:utils/observablemixin~Observable}
+ * interface (like {@link module:ui/model~Model}).
+ *
+ * Once bound, the observable will immediately share the current state of properties
+ * of the observable it is bound to and react to the changes to these properties
+ * in the future.
+ *
+ * **Note**: To release the binding use {@link module:utils/observablemixin~Observable#unbind}.
+ *
+ * Using `bind().to()` chain:
+ *
+ *		A.bind( 'a' ).to( B );
+ *		A.bind( 'a' ).to( B, 'b' );
+ *		A.bind( 'a', 'b' ).to( B, 'c', 'd' );
+ *		A.bind( 'a' ).to( B, 'b', C, 'd', ( b, d ) => b + d );
+ *
+ * It is also possible to bind to the same property in a observables collection using `bind().toMany()` chain:
+ *
+ *		A.bind( 'a' ).toMany( [ B, C, D ], 'x', ( a, b, c ) => a + b + c );
+ *		A.bind( 'a' ).toMany( [ B, C, D ], 'x', ( ...x ) => x.every( x => x ) );
+ *
+ * @method #bind
+ * @param {...String} bindProperties Observable properties that will be bound to another observable(s).
+ * @returns {Object} The bind chain with the `to()` and `toMany()` methods.
+ */
+
+/**
+ * Removes the binding created with {@link #bind}.
+ *
+ *		A.unbind( 'a' );
+ *		A.unbind();
+ *
+ * @method #unbind
+ * @param {...String} [unbindProperties] Observable properties to be unbound. All the bindings will
+ * be released if no properties provided.
+ */
+
+/**
+ * Turns the given methods of this object into event-based ones. This means that the new method will fire an event
+ * (named after the method) and the original action will be plugged as a listener to that event.
+ *
+ * This is a very simplified method decoration. Itself it doesn't change the behavior of a method (expect adding the event),
+ * but it allows to modify it later on by listening to the method's event.
+ *
+ * For example, in order to cancel the method execution one can stop the event:
+ *
+ *		class Foo {
+ *			constructor() {
+ *				this.decorate( 'method' );
+ *			}
+ *
+ *			method() {
+ *				console.log( 'called!' );
+ *			}
+ *		}
+ *
+ *		const foo = new Foo();
+ *		foo.on( 'method', ( evt ) => {
+ *			evt.stop();
+ *		}, { priority: 'high' } );
+ *
+ *		foo.method(); // Nothing is logged.
+ *
+ *
+ * Note: we used a high priority listener here to execute this callback before the one which
+ * calls the original method (which used the default priority).
+ *
+ * It's also possible to change the return value:
+ *
+ *		foo.on( 'method', ( evt ) => {
+ *			evt.return = 'Foo!';
+ *		} );
+ *
+ *		foo.method(); // -> 'Foo'
+ *
+ * Finally, it's possible to access and modify the parameters:
+ *
+ *		method( a, b ) {
+ *			console.log( `${ a }, ${ b }`  );
+ *		}
+ *
+ *		// ...
+ *
+ *		foo.on( 'method', ( evt, args ) => {
+ *			args[ 0 ] = 3;
+ *
+ *			console.log( args[ 1 ] ); // -> 2
+ *		}, { priority: 'high' } );
+ *
+ *		foo.method( 1, 2 ); // -> '3, 2'
+ *
+ * @method #decorate
+ * @param {String} methodName Name of the method to decorate.
  */

+ 1 - 1
packages/ckeditor5-utils/src/priorities.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/spy.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/src/tomap.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 32 - 6
packages/ckeditor5-utils/src/translation-service.js

@@ -1,8 +1,10 @@
 /**
- * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
+/* globals window */
+
 /**
  * @module utils/translation-service
  */
@@ -18,6 +20,12 @@ let dictionaries = {};
  *			'Cancel [context: reject]': 'Anuluj'
  *		} );
  *
+ * That function is accessible globally via `window.CKEDITOR_TRANSLATIONS.add()`. So it's possible to add translation from
+ * the other script, just after that one.
+ *
+ * 		<script src="./path/to/ckeditor.js"></script>
+ * 		<script src="./path/to/translations/en.js"></script>
+ *
  * @param {String} lang Target language.
  * @param {Object.<String, String>} translations Translations which will be added to the dictionary.
  */
@@ -34,24 +42,33 @@ export function add( lang, translations ) {
  * When no translation is defined in the dictionary or the dictionary doesn't exist this function returns
  * the original string without the `'[context: ]'` (happens in development and single-language modes).
  *
- * In a single-language mode (when values passed to `t()` were replaced with target languange strings) the dictionary
+ * In a single-language mode (when values passed to `t()` were replaced with target language strings) the dictionary
  * is left empty, so this function will return the original strings always.
  *
  *		translate( 'pl', 'Cancel [context: reject]' );
  *
  * @param {String} lang Target language.
- * @param {String} translationKey String which is going to be translated.
+ * @param {String} translationKey String that will be translated.
  * @returns {String} Translated sentence.
  */
 export function translate( lang, translationKey ) {
-	if ( !hasTranslation( lang, translationKey ) ) {
+	const numberOfLanguages = getNumberOfLanguages();
+
+	if ( numberOfLanguages === 1 ) {
+		// Override the language to the only supported one.
+		// This can't be done in the `Locale` class, because the translations comes after the `Locale` class initialization.
+		lang = Object.keys( dictionaries )[ 0 ];
+	}
+
+	if ( numberOfLanguages === 0 || !hasTranslation( lang, translationKey ) ) {
 		return translationKey.replace( / \[context: [^\]]+\]$/, '' );
 	}
 
-	return dictionaries[ lang ][ translationKey ];
+	// In case of missing translations we still need to cut off the `[context: ]` parts.
+	return dictionaries[ lang ][ translationKey ].replace( / \[context: [^\]]+\]$/, '' );
 }
 
-// Checks whether the dictionary exists and translaiton in that dictionary exists.
+// Checks whether the dictionary exists and translation in that dictionary exists.
 function hasTranslation( lang, translationKey ) {
 	return (
 		( lang in dictionaries ) &&
@@ -67,3 +84,12 @@ function hasTranslation( lang, translationKey ) {
 export function _clear() {
 	dictionaries = {};
 }
+
+function getNumberOfLanguages() {
+	return Object.keys( dictionaries ).length;
+}
+
+// Export globally add function to enable adding later translations.
+// See https://github.com/ckeditor/ckeditor5/issues/624
+window.CKEDITOR_TRANSLATIONS = window.CKEDITOR_TRANSLATIONS || {};
+window.CKEDITOR_TRANSLATIONS.add = add;

+ 4 - 4
packages/ckeditor5-utils/src/uid.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -8,10 +8,10 @@
  */
 
 /**
- * Returns a unique id. This id is a number (starting from 1) which will never get repeated on successive calls
- * to this method.
+ * Returns a unique id. This id consist of an 'e' character and a randomly generated string of 32 aphanumeric characters.
+ * Each character in uid string represents a hexadecimal digit (base 16).
  *
- * @returns {String} A number representing the id.
+ * @returns {String} A hexadecimal number representing the id.
  */
 export default function uid() {
 	let uuid = 'e'; // Make sure that id does not start with number.

+ 1 - 1
packages/ckeditor5-utils/src/unicode.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 9 - 9
packages/ckeditor5-utils/tests/_utils-tests/normalizehtml.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -8,29 +8,29 @@ import normalizeHtml from '../../tests/_utils/normalizehtml';
 describe( 'utils', () => {
 	describe( 'normalizeHtml', () => {
 		it( 'should sort attributes', () => {
-			const actual = '<a style="border:1px;" class="" href="file://"></a>';
-			const expected = '<a class="" href="file://" style="border:1px;"></a>';
+			const actual = '<a style="border:1px" class="" href="file://"></a>';
+			const expected = '<a class="" href="file://" style="border:1px"></a>';
 
 			expect( normalizeHtml( actual ) ).to.equal( expected );
 		} );
 
 		it( 'should normalize styles', () => {
 			const actual = '<a style="border:1px"></a>';
-			const expected = '<a style="border:1px;"></a>';
+			const expected = '<a style="border:1px"></a>';
 
 			expect( normalizeHtml( actual ) ).to.equal( expected );
 		} );
 
 		it( 'should lowercase attributes', () => {
-			const actual = '<A CLASS="" HREF="file://" STYLE="border:1px;"></A>';
-			const expected = '<a class="" href="file://" style="border:1px;"></a>';
+			const actual = '<A CLASS="" HREF="file://" STYLE="border:1px"></A>';
+			const expected = '<a class="" href="file://" style="border:1px"></a>';
 
 			expect( normalizeHtml( actual ) ).to.equal( expected );
 		} );
 
 		it( 'should trim whitespace', () => {
 			const actual = '<a class="  " href="file://"      style="border:  1px"></a>';
-			const expected = '<a class="" href="file://" style="border:1px;"></a>';
+			const expected = '<a class="" href="file://" style="border:1px"></a>';
 
 			expect( normalizeHtml( actual ) ).to.equal( expected );
 		} );
@@ -49,9 +49,9 @@ describe( 'utils', () => {
 			expect( normalizeHtml( actual ) ).to.equal( expected );
 		} );
 
-		it( 'should not sort attribute value', () => {
+		it( 'should sort attribute value', () => {
 			const actual = '<a class="b c a"></a>';
-			const expected = actual;
+			const expected = '<a class="a b c"></a>';
 
 			expect( normalizeHtml( actual ) ).to.equal( expected );
 		} );

+ 1 - 1
packages/ckeditor5-utils/tests/_utils-tests/utils.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/tests/_utils/locale-mock.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/tests/_utils/normalizehtml.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/tests/_utils/utils.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 3 - 3
packages/ckeditor5-utils/tests/ckeditorerror.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -56,7 +56,7 @@ describe( 'CKEditorError', () => {
 		const error = new CKEditorError( 'model-schema-no-item: Specified item cannot be found.' );
 
 		const errorMessage = 'model-schema-no-item: Specified item cannot be found. ' +
-			`Read more: ${ DOCUMENTATION_URL }#model-schema-no-item\n`;
+			`Read more: ${ DOCUMENTATION_URL }#error-model-schema-no-item\n`;
 
 		expect( error ).to.have.property( 'message', errorMessage );
 	} );
@@ -65,7 +65,7 @@ describe( 'CKEditorError', () => {
 		const error = new CKEditorError( 'model-schema-no-item: Specified item cannot be found.', { foo: 1, bar: 2 } );
 
 		const errorMessage = 'model-schema-no-item: Specified item cannot be found. ' +
-			`Read more: ${ DOCUMENTATION_URL }#model-schema-no-item\n ` +
+			`Read more: ${ DOCUMENTATION_URL }#error-model-schema-no-item\n ` +
 			'{"foo":1,"bar":2}';
 
 		expect( error ).to.have.property( 'message', errorMessage );

+ 160 - 4
packages/ckeditor5-utils/tests/collection.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -448,9 +448,9 @@ describe( 'Collection', () => {
 			collection.remove( 'bom' );	// by id
 
 			sinon.assert.calledThrice( spy );
-			sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', collection ), item1 );
-			sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', collection ), item2 );
-			sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', collection ), item3 );
+			sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', collection ), item1, 0 );
+			sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', collection ), item2, 1 );
+			sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', collection ), item3, 0 );
 		} );
 
 		it( 'should throw an error on invalid index', () => {
@@ -767,6 +767,34 @@ describe( 'Collection', () => {
 					expect( collection.get( 0 ).value ).to.equal( 'foo' );
 					expect( collection.get( 1 ).value ).to.equal( 'bar' );
 				} );
+
+				it( 'skips when there is no item', () => {
+					// Add before collection is bound.
+					items.add( { value: 1, skip: true } );
+
+					expect( collection ).to.have.length( 0 );
+
+					collection.bindTo( items ).using( item => {
+						if ( item.skip ) {
+							return null;
+						}
+
+						return item;
+					} );
+
+					// Still 0 because initial item was skipped.
+					expect( collection ).to.have.length( 0 );
+
+					items.add( { value: 2, skip: false } );
+					items.add( { value: 3, skip: true } );
+					items.add( { value: 4, skip: false } );
+
+					expect( Array.from( collection, item => item.value ) ).to.deep.equal( [ 2, 4 ] );
+
+					items.add( { value: 5, skip: false }, 2 );
+
+					expect( Array.from( collection, item => item.value ) ).to.deep.equal( [ 2, 5, 4 ] );
+				} );
 			} );
 
 			describe( 'property name', () => {
@@ -802,6 +830,25 @@ describe( 'Collection', () => {
 					items.remove( 0 );
 					expect( collection ).to.have.length( 0 );
 				} );
+
+				it( 'skips when there is no item', () => {
+					items.add( { prop: null } );
+
+					collection.bindTo( items ).using( 'prop' );
+
+					// Still 0 because initial item was skipped.
+					expect( collection ).to.have.length( 0 );
+
+					items.add( { prop: { value: 2, skip: false } } );
+					items.add( { prop: null } );
+					items.add( { prop: { value: 4, skip: false } } );
+
+					expect( Array.from( collection, item => item.value ) ).to.deep.equal( [ 2, 4 ] );
+
+					items.add( { prop: { value: 5 } }, 2 );
+
+					expect( Array.from( collection, item => item.value ) ).to.deep.equal( [ 2, 5, 4 ] );
+				} );
 			} );
 		} );
 
@@ -1032,6 +1079,115 @@ describe( 'Collection', () => {
 				sinon.assert.callCount( spyRemoveA, 2 );
 				sinon.assert.callCount( spyRemoveB, 2 );
 			} );
+
+			describe( 'skipping items', () => {
+				let collectionA, collectionB;
+
+				beforeEach( () => {
+					collectionA = new Collection();
+					collectionB = new Collection();
+
+					// A<--->B
+					collectionA.bindTo( collectionB ).using( item => {
+						if ( item.skip ) {
+							return null;
+						}
+
+						return item;
+					} );
+
+					collectionB.bindTo( collectionA ).using( item => {
+						if ( item.skip ) {
+							return null;
+						}
+
+						return item;
+					} );
+				} );
+
+				it( 'should add items at the enf of collections when includes skipped items', () => {
+					collectionA.add( { v: 'A' } );
+					collectionA.add( { v: 'B', skip: true } );
+					collectionA.add( { v: 'C', skip: true } );
+					collectionA.add( { v: 'D' } );
+
+					expect( collectionA._skippedIndexesFromExternal ).to.have.members( [] );
+					expect( collectionB._skippedIndexesFromExternal ).to.have.members( [ 1, 2 ] );
+					assertItems( collectionA, [ 'A', 'B', 'C', 'D' ] );
+					assertItems( collectionB, [ 'A', 'D' ] );
+
+					collectionB.add( { v: 'E' } );
+					collectionB.add( { v: 'F', skip: true } );
+					collectionB.add( { v: 'G' } );
+
+					expect( collectionA._skippedIndexesFromExternal ).to.have.members( [ 3 ] );
+					expect( collectionB._skippedIndexesFromExternal ).to.have.members( [ 1, 2 ] );
+					assertItems( collectionA, [ 'A', 'B', 'C', 'D', 'E', 'G' ] );
+					assertItems( collectionB, [ 'A', 'D', 'E', 'F', 'G' ] );
+				} );
+
+				it( 'should add items between skipped items', () => {
+					collectionA.add( { v: 'A' } );
+					collectionA.add( { v: 'B', skip: true } );
+					collectionA.add( { v: 'C', skip: true } );
+					collectionA.add( { v: 'D' } );
+
+					collectionB.add( { v: 'E' } );
+					collectionB.add( { v: 'F', skip: true } );
+					collectionB.add( { v: 'G', skip: true } );
+					collectionB.add( { v: 'H' } );
+
+					expect( collectionA._skippedIndexesFromExternal ).to.have.members( [ 3, 4 ] );
+					expect( collectionB._skippedIndexesFromExternal ).to.have.members( [ 1, 2 ] );
+					assertItems( collectionA, [ 'A', 'B', 'C', 'D', 'E', 'H' ] );
+					assertItems( collectionB, [ 'A', 'D', 'E', 'F', 'G', 'H' ] );
+
+					collectionA.add( { v: 'I' }, 2 );
+
+					expect( collectionA._skippedIndexesFromExternal ).to.have.members( [ 4, 5 ] );
+					expect( collectionB._skippedIndexesFromExternal ).to.have.members( [ 1, 2 ] );
+					assertItems( collectionA, [ 'A', 'B', 'I', 'C', 'D', 'E', 'H' ] );
+					assertItems( collectionB, [ 'A', 'I', 'D', 'E', 'F', 'G', 'H' ] );
+
+					collectionB.add( { v: 'J' }, 5 );
+
+					expect( collectionA._skippedIndexesFromExternal ).to.have.members( [ 4, 5 ] );
+					expect( collectionB._skippedIndexesFromExternal ).to.have.members( [ 1, 2 ] );
+					assertItems( collectionA, [ 'A', 'B', 'I', 'C', 'D', 'E', 'J', 'H' ] );
+					assertItems( collectionB, [ 'A', 'I', 'D', 'E', 'F', 'J', 'G', 'H' ] );
+				} );
+
+				it( 'should properly remove skipped items and update skipped indexes', () => {
+					collectionA.add( { v: 'A' } );
+					collectionA.add( { v: 'B', skip: true } );
+					collectionA.add( { v: 'C', skip: true } );
+					collectionA.add( { v: 'D' } );
+
+					collectionB.add( { v: 'E' } );
+					collectionB.add( { v: 'F', skip: true } );
+					collectionB.add( { v: 'G', skip: true } );
+					collectionB.add( { v: 'H' } );
+
+					expect( collectionA._skippedIndexesFromExternal ).to.have.members( [ 3, 4 ] );
+					expect( collectionB._skippedIndexesFromExternal ).to.have.members( [ 1, 2 ] );
+					assertItems( collectionA, [ 'A', 'B', 'C', 'D', 'E', 'H' ] );
+					assertItems( collectionB, [ 'A', 'D', 'E', 'F', 'G', 'H' ] );
+
+					collectionA.remove( 2 );
+
+					expect( collectionA._skippedIndexesFromExternal ).to.have.members( [ 3, 4 ] );
+					expect( collectionB._skippedIndexesFromExternal ).to.have.members( [ 1 ] );
+					assertItems( collectionA, [ 'A', 'B', 'D', 'E', 'H' ] );
+					assertItems( collectionB, [ 'A', 'D', 'E', 'F', 'G', 'H' ] );
+
+					collectionB.remove( 3 );
+
+					expect( collectionA._skippedIndexesFromExternal ).to.have.members( [ 3 ] );
+					expect( collectionB._skippedIndexesFromExternal ).to.have.members( [ 1 ] );
+					assertItems( collectionA, [ 'A', 'B', 'D', 'E', 'H' ] );
+					assertItems( collectionB, [ 'A', 'D', 'E', 'G', 'H' ] );
+				} );
+			} );
 		} );
 	} );
 

+ 1 - 1
packages/ckeditor5-utils/tests/comparearrays.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/tests/config.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/tests/count.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/tests/diff.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/tests/difftochanges.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/tests/dom/createelement.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 104 - 1
packages/ckeditor5-utils/tests/dom/emittermixin.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -116,6 +116,46 @@ describe( 'DomEmitterMixin', () => {
 	} );
 
 	describe( 'stopListening', () => {
+		it( 'should stop listening to EmitterMixin events, specific event', () => {
+			const spy1 = testUtils.sinon.spy();
+			const spy2 = testUtils.sinon.spy();
+
+			domEmitter.listenTo( emitter, 'foo', spy1 );
+			domEmitter.listenTo( emitter, 'foo:bar', spy2 );
+
+			emitter.fire( 'foo:bar' );
+
+			sinon.assert.calledOnce( spy1 );
+			sinon.assert.calledOnce( spy2 );
+
+			domEmitter.stopListening( emitter, 'foo' );
+
+			emitter.fire( 'foo:bar' );
+
+			sinon.assert.calledOnce( spy1 );
+			sinon.assert.calledTwice( spy2 );
+		} );
+
+		it( 'should stop listening to EmitterMixin events, specific emitter', () => {
+			const spy1 = testUtils.sinon.spy();
+			const spy2 = testUtils.sinon.spy();
+
+			domEmitter.listenTo( emitter, 'foo', spy1 );
+			domEmitter.listenTo( emitter, 'foo:bar', spy2 );
+
+			emitter.fire( 'foo:bar' );
+
+			sinon.assert.calledOnce( spy1 );
+			sinon.assert.calledOnce( spy2 );
+
+			domEmitter.stopListening( emitter );
+
+			emitter.fire( 'foo:bar' );
+
+			sinon.assert.calledOnce( spy1 );
+			sinon.assert.calledOnce( spy2 );
+		} );
+
 		it( 'should stop listening to a specific event callback', () => {
 			const spy1 = testUtils.sinon.spy();
 			const spy2 = testUtils.sinon.spy();
@@ -135,6 +175,34 @@ describe( 'DomEmitterMixin', () => {
 			sinon.assert.calledTwice( spy2 );
 		} );
 
+		it( 'should stop listening to a specific event callback (only from the given node)', () => {
+			const spy1 = testUtils.sinon.spy();
+			const spy2 = testUtils.sinon.spy();
+
+			const node1 = document.createElement( 'div' );
+			const node2 = document.createElement( 'div' );
+
+			domEmitter.listenTo( node1, 'event1', spy1 );
+			domEmitter.listenTo( node1, 'event2', spy2 );
+			domEmitter.listenTo( node2, 'event1', spy1 );
+
+			node1.dispatchEvent( new Event( 'event1' ) );
+			node1.dispatchEvent( new Event( 'event2' ) );
+			node2.dispatchEvent( new Event( 'event1' ) );
+
+			sinon.assert.calledTwice( spy1 );
+			sinon.assert.calledOnce( spy2 );
+
+			domEmitter.stopListening( node1, 'event1', spy1 );
+
+			node1.dispatchEvent( new Event( 'event1' ) );
+			node1.dispatchEvent( new Event( 'event2' ) );
+			node2.dispatchEvent( new Event( 'event1' ) );
+
+			sinon.assert.calledThrice( spy1 );
+			sinon.assert.calledTwice( spy2 );
+		} );
+
 		it( 'should stop listening to a specific event callback (re–listen)', () => {
 			const spy = testUtils.sinon.spy();
 
@@ -192,26 +260,53 @@ describe( 'DomEmitterMixin', () => {
 			sinon.assert.calledOnce( spy2 );
 		} );
 
+		it( 'should stop listening to all events from a specific node (only that node)', () => {
+			const spy1 = testUtils.sinon.spy();
+			const spy2 = testUtils.sinon.spy();
+
+			const node1 = document.createElement( 'div' );
+			const node2 = document.createElement( 'div' );
+
+			domEmitter.listenTo( node1, 'event', spy1 );
+			domEmitter.listenTo( node2, 'event', spy2 );
+
+			node1.dispatchEvent( new Event( 'event' ) );
+			node2.dispatchEvent( new Event( 'event' ) );
+
+			domEmitter.stopListening( node1 );
+
+			node1.dispatchEvent( new Event( 'event' ) );
+			node2.dispatchEvent( new Event( 'event' ) );
+
+			sinon.assert.calledOnce( spy1 );
+			sinon.assert.calledTwice( spy2 );
+		} );
+
 		it( 'should stop listening to everything', () => {
 			const spy1 = testUtils.sinon.spy();
 			const spy2 = testUtils.sinon.spy();
+			const spy3 = testUtils.sinon.spy();
 
 			const node1 = document.createElement( 'div' );
 			const node2 = document.createElement( 'div' );
 
 			domEmitter.listenTo( node1, 'event1', spy1 );
 			domEmitter.listenTo( node2, 'event2', spy2 );
+			domEmitter.listenTo( emitter, 'event3', spy3 );
 
 			node1.dispatchEvent( new Event( 'event1' ) );
 			node2.dispatchEvent( new Event( 'event2' ) );
+			emitter.fire( 'event3' );
 
 			domEmitter.stopListening();
 
 			node1.dispatchEvent( new Event( 'event1' ) );
 			node2.dispatchEvent( new Event( 'event2' ) );
+			emitter.fire( 'event3' );
 
 			sinon.assert.calledOnce( spy1 );
 			sinon.assert.calledOnce( spy2 );
+			sinon.assert.calledOnce( spy3 );
 		} );
 
 		it( 'should stop listening to everything what left', () => {
@@ -413,28 +508,35 @@ describe( 'DomEmitterMixin', () => {
 			const spy1b = testUtils.sinon.spy();
 			const spy1c = testUtils.sinon.spy();
 			const spy1d = testUtils.sinon.spy();
+			const spyEl2 = testUtils.sinon.spy();
+			const node2 = document.createElement( 'div' );
 
 			domEmitter.listenTo( node, 'test1', spy1a );
 			domEmitter.listenTo( node, 'test2', spy1b );
+			domEmitter.listenTo( node2, 'test1', spyEl2 );
 
 			const proxyEmitter = domEmitter._getProxyEmitter( node );
 			const spy2 = testUtils.sinon.spy( proxyEmitter, 'fire' );
 
 			node.dispatchEvent( new Event( 'test1' ) );
 			node.dispatchEvent( new Event( 'test2' ) );
+			node2.dispatchEvent( new Event( 'test1' ) );
 
 			sinon.assert.calledOnce( spy1a );
 			sinon.assert.calledOnce( spy1b );
 			sinon.assert.calledTwice( spy2 );
+			sinon.assert.calledOnce( spyEl2 );
 
 			domEmitter.stopListening();
 
 			node.dispatchEvent( new Event( 'test1' ) );
 			node.dispatchEvent( new Event( 'test2' ) );
+			node2.dispatchEvent( new Event( 'test1' ) );
 
 			sinon.assert.calledOnce( spy1a );
 			sinon.assert.calledOnce( spy1b );
 			sinon.assert.calledTwice( spy2 );
+			sinon.assert.calledOnce( spyEl2 );
 
 			// Attach same event again.
 			domEmitter.listenTo( node, 'test1', spy1c );
@@ -455,6 +557,7 @@ describe( 'DomEmitterMixin', () => {
 			sinon.assert.calledOnce( spy1d );
 			sinon.assert.calledTwice( spy2 );
 			sinon.assert.calledTwice( spy3 );
+			sinon.assert.calledOnce( spyEl2 );
 		} );
 
 		// #187

+ 1 - 1
packages/ckeditor5-utils/tests/dom/getancestors.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/tests/dom/getborderwidths.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/tests/dom/getcommonancestor.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/tests/dom/getdatafromelement.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/tests/dom/getpositionedancestor.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/tests/dom/global.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/tests/dom/indexof.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/tests/dom/insertat.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 32 - 12
packages/ckeditor5-utils/tests/dom/isdomnode.js

@@ -1,22 +1,42 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 /* global document, window */
 
-import isDomNode from '../../src/dom/isdomnode';
+import isNode from '../../src/dom/isnode';
 
-describe( 'isDomNode()', () => {
+describe( 'isNode()', () => {
 	it( 'detects native DOM nodes', () => {
-		expect( isDomNode( {} ) ).to.be.false;
-		expect( isDomNode( null ) ).to.be.false;
-		expect( isDomNode( undefined ) ).to.be.false;
-		expect( isDomNode( new Date() ) ).to.be.false;
-		expect( isDomNode( 42 ) ).to.be.false;
-		expect( isDomNode( window ) ).to.be.true;
-		expect( isDomNode( document ) ).to.be.true;
-		expect( isDomNode( document.createElement( 'div' ) ) ).to.be.true;
-		expect( isDomNode( document.createTextNode( 'Foo' ) ) ).to.be.true;
+		expect( isNode( document ) ).to.be.true;
+		expect( isNode( document.createElement( 'div' ) ) ).to.be.true;
+		expect( isNode( document.createTextNode( 'Foo' ) ) ).to.be.true;
+
+		expect( isNode( {} ) ).to.be.false;
+		expect( isNode( null ) ).to.be.false;
+		expect( isNode( undefined ) ).to.be.false;
+		expect( isNode( new Date() ) ).to.be.false;
+		expect( isNode( 42 ) ).to.be.false;
+		expect( isNode( window ) ).to.be.false;
+	} );
+
+	it( 'works for nodes in an iframe', done => {
+		const iframe = document.createElement( 'iframe' );
+
+		iframe.addEventListener( 'load', () => {
+			const iframeDocument = iframe.contentWindow.document;
+
+			expect( isNode( iframeDocument ) ).to.be.true;
+			expect( isNode( iframeDocument.createElement( 'div' ) ) ).to.be.true;
+			expect( isNode( iframeDocument.createTextNode( 'Foo' ) ) ).to.be.true;
+
+			expect( isNode( iframe.contentWindow ) ).to.be.false;
+
+			iframe.remove();
+			done();
+		} );
+
+		document.body.appendChild( iframe );
 	} );
 } );

+ 1 - 1
packages/ckeditor5-utils/tests/dom/isrange.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 41 - 0
packages/ckeditor5-utils/tests/dom/istext.js

@@ -0,0 +1,41 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document, Text */
+
+import isText from '../../src/dom/istext';
+
+describe( 'isText()', () => {
+	it( 'detects native DOM Text', () => {
+		expect( isText( new Text( 'foo' ) ) ).to.be.true;
+
+		expect( isText( 'foo' ) ).to.be.false;
+		expect( isText( {} ) ).to.be.false;
+		expect( isText( null ) ).to.be.false;
+		expect( isText( undefined ) ).to.be.false;
+		expect( isText( new Date() ) ).to.be.false;
+		expect( isText( 42 ) ).to.be.false;
+		expect( isText( document.createElement( 'div' ) ) ).to.be.false;
+		expect( isText( document.createDocumentFragment() ) ).to.be.false;
+		expect( isText( document.createComment( 'a' ) ) ).to.be.false;
+	} );
+
+	it( 'works for texts in an iframe', done => {
+		const iframe = document.createElement( 'iframe' );
+
+		iframe.addEventListener( 'load', () => {
+			const iframeDocument = iframe.contentWindow.document;
+
+			const textNode = iframeDocument.createTextNode( 'foo' );
+
+			expect( isText( textNode ) ).to.equal( true );
+
+			iframe.remove();
+			done();
+		} );
+
+		document.body.appendChild( iframe );
+	} );
+} );

+ 1 - 1
packages/ckeditor5-utils/tests/dom/iswindow.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/tests/dom/position.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 27 - 3
packages/ckeditor5-utils/tests/dom/rect.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -435,7 +435,7 @@ describe( 'Rect', () => {
 			ancestorA = document.createElement( 'div' );
 			ancestorB = document.createElement( 'div' );
 
-			ancestorA.append( element );
+			ancestorA.appendChild( element );
 			document.body.appendChild( ancestorA );
 		} );
 
@@ -575,7 +575,7 @@ describe( 'Rect', () => {
 		} );
 
 		it( 'should return the visible rect (HTMLElement), partially cropped, deep ancestor overflow', () => {
-			ancestorB.append( ancestorA );
+			ancestorB.appendChild( ancestorA );
 			document.body.appendChild( ancestorB );
 
 			testUtils.sinon.stub( element, 'getBoundingClientRect' ).returns( {
@@ -909,6 +909,9 @@ describe( 'Rect', () => {
 					height: 230
 				} );
 
+				// Safari fails because of "afterEach()" hook tries to restore values from removed element.
+				// We need to restore these values manually.
+				testUtils.sinon.restore();
 				iframe.remove();
 				done();
 			} );
@@ -962,6 +965,27 @@ describe( 'Rect', () => {
 			expect( rects ).to.have.length( 1 );
 			assertRect( rects[ 0 ], expectedGeometry );
 		} );
+
+		// https://github.com/ckeditor/ckeditor5-ui/issues/317
+		it( 'should return rects for a text node\'s parent (collapsed, no Range rects available)', () => {
+			const range = document.createRange();
+			const element = document.createElement( 'div' );
+			const textNode = document.createTextNode( 'abc' );
+			element.appendChild( textNode );
+
+			range.setStart( textNode, 3 );
+			range.collapse();
+			testUtils.sinon.stub( range, 'getClientRects' ).returns( [] );
+			testUtils.sinon.stub( element, 'getBoundingClientRect' ).returns( geometry );
+
+			const expectedGeometry = Object.assign( {}, geometry );
+			expectedGeometry.right = expectedGeometry.left;
+			expectedGeometry.width = 0;
+
+			const rects = Rect.getDomRangeRects( range );
+			expect( rects ).to.have.length( 1 );
+			assertRect( rects[ 0 ], expectedGeometry );
+		} );
 	} );
 } );
 

+ 1 - 1
packages/ckeditor5-utils/tests/dom/remove.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 4 - 1
packages/ckeditor5-utils/tests/dom/scroll.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -248,6 +248,9 @@ describe( 'scrollViewportToShowTarget()', () => {
 		} );
 
 		afterEach( () => {
+			// Safari fails because of "afterEach()" hook tries to restore values from removed element.
+			// We need to restore these values manually.
+			testUtils.sinon.restore();
 			iframeAncestor.remove();
 		} );
 

+ 1 - 1
packages/ckeditor5-utils/tests/dom/setdatainelement.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/tests/dom/tounit.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/tests/elementreplacer.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 169 - 7
packages/ckeditor5-utils/tests/emittermixin.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -280,7 +280,7 @@ describe( 'EmitterMixin', () => {
 	} );
 
 	describe( 'once', () => {
-		it( 'should be called just once', () => {
+		it( 'should be called just once for general event', () => {
 			const spy1 = sinon.spy();
 			const spy2 = sinon.spy();
 			const spy3 = sinon.spy();
@@ -297,6 +297,23 @@ describe( 'EmitterMixin', () => {
 			sinon.assert.calledTwice( spy3 );
 		} );
 
+		it( 'should be called just once for namespaced event', () => {
+			const spy1 = sinon.spy();
+			const spy2 = sinon.spy();
+			const spy3 = sinon.spy();
+
+			emitter.on( 'foo:bar', spy1 );
+			emitter.once( 'foo:bar', spy2 );
+			emitter.on( 'foo:bar', spy3 );
+
+			emitter.fire( 'foo:bar' );
+			emitter.fire( 'foo:bar' );
+
+			sinon.assert.calledTwice( spy1 );
+			sinon.assert.calledOnce( spy2 );
+			sinon.assert.calledTwice( spy3 );
+		} );
+
 		it( 'should have proper arguments', () => {
 			const spy = sinon.spy();
 
@@ -306,6 +323,22 @@ describe( 'EmitterMixin', () => {
 
 			sinon.assert.calledWithExactly( spy, sinon.match.instanceOf( EventInfo ), 1, 2, 3 );
 		} );
+
+		it( 'should be removed only after exact event fired', () => {
+			const spy1 = sinon.spy();
+			const spy2 = sinon.spy();
+
+			emitter.on( 'foo', spy1 );
+			emitter.once( 'foo', spy2 );
+
+			emitter.fire( 'foo:bar' );
+			emitter.fire( 'foo' );
+			emitter.fire( 'foo:bar' );
+			emitter.fire( 'foo' );
+
+			sinon.assert.callCount( spy1, 4 );
+			sinon.assert.calledTwice( spy2 );
+		} );
 	} );
 
 	describe( 'off', () => {
@@ -330,8 +363,30 @@ describe( 'EmitterMixin', () => {
 			sinon.assert.calledThrice( spy3 );
 		} );
 
+		it( 'should remove all callbacks for event', () => {
+			const spy1 = sinon.spy();
+			const spy2 = sinon.spy();
+
+			emitter.on( 'test', spy1 );
+			emitter.on( 'test', spy2 );
+
+			emitter.fire( 'test' );
+
+			emitter.off( 'test' );
+
+			emitter.fire( 'test' );
+			emitter.fire( 'test' );
+
+			sinon.assert.calledOnce( spy1 );
+			sinon.assert.calledOnce( spy2 );
+		} );
+
 		it( 'should not fail with unknown events', () => {
-			emitter.off( 'test', () => {} );
+			emitter.off( 'foo', () => {} );
+			emitter.off( 'foo:bar', () => {} );
+
+			emitter.off( 'foo' );
+			emitter.off( 'foo:bar' );
 		} );
 
 		it( 'should remove all entries for the same callback', () => {
@@ -353,6 +408,34 @@ describe( 'EmitterMixin', () => {
 			sinon.assert.callCount( spy2, 4 );
 		} );
 
+		it( 'should not remove all namespaced entries when removing namespace inner group', () => {
+			const spy1 = sinon.spy().named( 'foo' );
+			const spy2 = sinon.spy().named( 'foo:bar' );
+			const spy3 = sinon.spy().named( 'foo:bar:baz' );
+			const spy4 = sinon.spy().named( 'foo:bar:baz:abc' );
+
+			emitter.on( 'foo', spy1 );
+			emitter.on( 'foo:bar', spy2 );
+			emitter.on( 'foo:bar:baz', spy3 );
+			emitter.on( 'foo:bar:baz:abc', spy4 );
+
+			emitter.fire( 'foo:bar:baz:abc' );
+
+			sinon.assert.calledOnce( spy1 );
+			sinon.assert.calledOnce( spy2 );
+			sinon.assert.calledOnce( spy3 );
+			sinon.assert.calledOnce( spy4 );
+
+			emitter.off( 'foo:bar' );
+
+			emitter.fire( 'foo:bar:baz:abc' );
+
+			sinon.assert.calledTwice( spy1 );
+			sinon.assert.calledOnce( spy2 );
+			sinon.assert.calledTwice( spy3 );
+			sinon.assert.calledTwice( spy4 );
+		} );
+
 		it( 'should properly remove callbacks for namespaced events', () => {
 			const spyFoo = sinon.spy();
 			const spyAbc = sinon.spy();
@@ -403,19 +486,29 @@ describe( 'EmitterMixin', () => {
 		it( 'should correctly listen to namespaced events', () => {
 			const spyFoo = sinon.spy();
 			const spyBar = sinon.spy();
+			const spyBaz = sinon.spy();
 
 			listener.listenTo( emitter, 'foo', spyFoo );
 			listener.listenTo( emitter, 'foo:bar', spyBar );
+			listener.listenTo( emitter, 'foo:bar:baz', spyBaz );
 
-			emitter.fire( 'foo:bar' );
+			emitter.fire( 'foo:bar:baz' );
 
 			sinon.assert.calledOnce( spyFoo );
 			sinon.assert.calledOnce( spyBar );
+			sinon.assert.calledOnce( spyBaz );
 
-			emitter.fire( 'foo' );
+			emitter.fire( 'foo:bar' );
 
 			sinon.assert.calledTwice( spyFoo );
-			sinon.assert.calledOnce( spyBar );
+			sinon.assert.calledTwice( spyBar );
+			sinon.assert.calledOnce( spyBaz );
+
+			emitter.fire( 'foo' );
+
+			sinon.assert.calledThrice( spyFoo );
+			sinon.assert.calledTwice( spyBar );
+			sinon.assert.calledOnce( spyBaz );
 		} );
 	} );
 
@@ -464,42 +557,62 @@ describe( 'EmitterMixin', () => {
 		it( 'should stop listening to all events from given emitter', () => {
 			const spy1 = sinon.spy();
 			const spy2 = sinon.spy();
+			const spy3 = sinon.spy();
+			const spy4 = sinon.spy();
 
 			listener.listenTo( emitter, 'event1', spy1 );
 			listener.listenTo( emitter, 'event2', spy2 );
+			listener.listenTo( emitter, 'foo', spy3 );
+			listener.listenTo( emitter, 'foo:bar:baz', spy4 );
 
 			emitter.fire( 'event1' );
 			emitter.fire( 'event2' );
+			emitter.fire( 'foo:bar:baz' );
 
 			listener.stopListening( emitter );
 
 			emitter.fire( 'event1' );
 			emitter.fire( 'event2' );
+			emitter.fire( 'foo:bar:baz' );
 
 			sinon.assert.calledOnce( spy1 );
 			sinon.assert.calledOnce( spy2 );
+			sinon.assert.calledOnce( spy3 );
+			sinon.assert.calledOnce( spy4 );
 		} );
 
 		it( 'should stop listening to everything', () => {
 			const spy1 = sinon.spy();
 			const spy2 = sinon.spy();
+			const spy3 = sinon.spy();
+			const spy4 = sinon.spy();
 
 			const emitter1 = getEmitterInstance();
 			const emitter2 = getEmitterInstance();
 
 			listener.listenTo( emitter1, 'event1', spy1 );
 			listener.listenTo( emitter2, 'event2', spy2 );
+			listener.listenTo( emitter1, 'foo', spy3 );
+			listener.listenTo( emitter1, 'foo:bar:baz', spy4 );
 
 			emitter1.fire( 'event1' );
 			emitter2.fire( 'event2' );
+			emitter1.fire( 'foo' );
+			emitter1.fire( 'foo:bar' );
+			emitter1.fire( 'foo:bar:baz' );
 
 			listener.stopListening();
 
 			emitter1.fire( 'event1' );
 			emitter2.fire( 'event2' );
+			emitter1.fire( 'foo' );
+			emitter1.fire( 'foo:bar' );
+			emitter1.fire( 'foo:bar:baz' );
 
 			sinon.assert.calledOnce( spy1 );
 			sinon.assert.calledOnce( spy2 );
+			sinon.assert.calledThrice( spy3 );
+			sinon.assert.calledOnce( spy4 );
 		} );
 
 		it( 'should not stop other emitters when a non-listened emitter is provided', () => {
@@ -520,16 +633,65 @@ describe( 'EmitterMixin', () => {
 		it( 'should correctly stop listening to namespaced events', () => {
 			const spyFoo = sinon.spy();
 			const spyBar = sinon.spy();
+			const spyBaz = sinon.spy();
 
 			listener.listenTo( emitter, 'foo', spyFoo );
 			listener.listenTo( emitter, 'foo:bar', spyBar );
+			listener.listenTo( emitter, 'foo:bar:baz', spyBaz );
 
 			listener.stopListening( emitter, 'foo' );
 
-			emitter.fire( 'foo:bar' );
+			emitter.fire( 'foo:bar:baz' );
 
 			sinon.assert.notCalled( spyFoo );
 			sinon.assert.calledOnce( spyBar );
+			sinon.assert.calledOnce( spyBaz );
+		} );
+
+		it( 'should correctly stop listening to namespaced events when removing specialised event', () => {
+			const spyFoo = sinon.spy();
+			const spyBar = sinon.spy();
+			const spyBaz = sinon.spy();
+
+			listener.listenTo( emitter, 'foo', spyFoo );
+			listener.listenTo( emitter, 'foo:bar', spyBar );
+			listener.listenTo( emitter, 'foo:bar:baz', spyBaz );
+
+			listener.stopListening( emitter, 'foo:bar' );
+
+			emitter.fire( 'foo:bar:baz' );
+
+			sinon.assert.calledOnce( spyFoo );
+			sinon.assert.notCalled( spyBar );
+			sinon.assert.calledOnce( spyBaz );
+		} );
+
+		it( 'should not fail with unknown events', () => {
+			listener.stopListening( emitter, 'foo', () => {} );
+			listener.stopListening( emitter, 'foo:bar', () => {} );
+			listener.stopListening( emitter, 'foo' );
+			listener.stopListening( emitter, 'foo:bar' );
+		} );
+
+		it( 'should not fail with unknown emitter', () => {
+			listener.listenTo( emitter, 'foo:bar', () => {} );
+
+			listener.stopListening( {}, 'foo', () => {} );
+			listener.stopListening( {}, 'foo:bar', () => {} );
+			listener.stopListening( {}, 'foo' );
+			listener.stopListening( {}, 'foo:bar' );
+			listener.stopListening( {} );
+		} );
+
+		it( 'should not fail with unknown callbacks', () => {
+			const spy = sinon.spy();
+
+			listener.listenTo( emitter, 'foo', spy );
+			listener.stopListening( emitter, 'foo', () => {} );
+
+			emitter.fire( 'foo' );
+
+			sinon.assert.calledOnce( spy );
 		} );
 	} );
 

+ 4 - 4
packages/ckeditor5-utils/tests/env.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -13,13 +13,13 @@ describe( 'Env', () => {
 		expect( env ).to.be.an( 'object' );
 	} );
 
-	describe( 'mac', () => {
+	describe( 'isMac', () => {
 		it( 'is a boolean', () => {
-			expect( env.mac ).to.be.a( 'boolean' );
+			expect( env.isMac ).to.be.a( 'boolean' );
 		} );
 	} );
 
-	describe( 'isMac', () => {
+	describe( 'isMac()', () => {
 		it( 'returns true for macintosh UA strings', () => {
 			expect( isMac( 'macintosh' ) ).to.be.true;
 			expect( isMac( 'foo macintosh bar' ) ).to.be.true;

+ 1 - 1
packages/ckeditor5-utils/tests/eventinfo.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/tests/first.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/tests/focustracker.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1 - 1
packages/ckeditor5-utils/tests/isIterable.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 27 - 6
packages/ckeditor5-utils/tests/keyboard.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -103,15 +103,15 @@ describe( 'Keyboard', () => {
 	} );
 
 	describe( 'getEnvKeystrokeText', () => {
-		const initialEnvMac = env.mac;
+		const initialEnvMac = env.isMac;
 
 		afterEach( () => {
-			env.mac = initialEnvMac;
+			env.isMac = initialEnvMac;
 		} );
 
 		describe( 'on Macintosh', () => {
 			beforeEach( () => {
-				env.mac = true;
+				env.isMac = true;
 			} );
 
 			it( 'replaces CTRL with ⌘', () => {
@@ -120,21 +120,42 @@ describe( 'Keyboard', () => {
 				expect( getEnvKeystrokeText( 'ctrl+A' ) ).to.equal( '⌘A' );
 			} );
 
+			it( 'replaces SHIFT with ⇧', () => {
+				expect( getEnvKeystrokeText( 'SHIFT' ) ).to.equal( '⇧' );
+				expect( getEnvKeystrokeText( 'SHIFT+A' ) ).to.equal( '⇧A' );
+				expect( getEnvKeystrokeText( 'shift+A' ) ).to.equal( '⇧A' );
+			} );
+
+			it( 'replaces ALT with ⌥', () => {
+				expect( getEnvKeystrokeText( 'ALT' ) ).to.equal( '⌥' );
+				expect( getEnvKeystrokeText( 'ALT+A' ) ).to.equal( '⌥A' );
+				expect( getEnvKeystrokeText( 'alt+A' ) ).to.equal( '⌥A' );
+			} );
+
+			it( 'work for multiple modifiers', () => {
+				expect( getEnvKeystrokeText( 'CTRL+SHIFT+X' ) ).to.equal( '⌘⇧X' );
+				expect( getEnvKeystrokeText( 'ALT+SHIFT+X' ) ).to.equal( '⌥⇧X' );
+			} );
+
 			it( 'does not touch other keys', () => {
-				expect( getEnvKeystrokeText( 'SHIFT+A' ) ).to.equal( 'SHIFT+A' );
+				expect( getEnvKeystrokeText( 'ESC+A' ) ).to.equal( 'ESC+A' );
+				expect( getEnvKeystrokeText( 'TAB' ) ).to.equal( 'TAB' );
 				expect( getEnvKeystrokeText( 'A' ) ).to.equal( 'A' );
+				expect( getEnvKeystrokeText( 'A+CTRL+B' ) ).to.equal( 'A+⌘B' );
 			} );
 		} );
 
 		describe( 'on non–Macintosh', () => {
 			beforeEach( () => {
-				env.mac = false;
+				env.isMac = false;
 			} );
 
 			it( 'does not touch anything', () => {
 				expect( getEnvKeystrokeText( 'CTRL+A' ) ).to.equal( 'CTRL+A' );
 				expect( getEnvKeystrokeText( 'ctrl+A' ) ).to.equal( 'ctrl+A' );
 				expect( getEnvKeystrokeText( 'SHIFT+A' ) ).to.equal( 'SHIFT+A' );
+				expect( getEnvKeystrokeText( 'alt+A' ) ).to.equal( 'alt+A' );
+				expect( getEnvKeystrokeText( 'CTRL+SHIFT+A' ) ).to.equal( 'CTRL+SHIFT+A' );
 				expect( getEnvKeystrokeText( 'A' ) ).to.equal( 'A' );
 			} );
 		} );

+ 1 - 1
packages/ckeditor5-utils/tests/keystrokehandler.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 5 - 5
packages/ckeditor5-utils/tests/locale.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -13,16 +13,16 @@ describe( 'Locale', () => {
 	} );
 
 	describe( 'constructor', () => {
-		it( 'sets the lang', () => {
+		it( 'sets the language', () => {
 			const locale = new Locale( 'pl' );
 
-			expect( locale ).to.have.property( 'lang', 'pl' );
+			expect( locale ).to.have.property( 'language', 'pl' );
 		} );
 
-		it( 'defaults lang to en', () => {
+		it( 'defaults language to en', () => {
 			const locale = new Locale();
 
-			expect( locale ).to.have.property( 'lang', 'en' );
+			expect( locale ).to.have.property( 'language', 'en' );
 		} );
 	} );
 

+ 1 - 1
packages/ckeditor5-utils/tests/lodash.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است