Преглед изворни кода

Merge branch 'master' into t/ckeditor5/416b

Aleksander Nowodzinski пре 6 година
родитељ
комит
fe7451fd12
24 измењених фајлова са 718 додато и 254 уклоњено
  1. 2 2
      packages/ckeditor5-editor-classic/.eslintrc.js
  2. 11 13
      packages/ckeditor5-editor-classic/.travis.yml
  3. 51 0
      packages/ckeditor5-editor-classic/CHANGELOG.md
  4. 1 1
      packages/ckeditor5-editor-classic/CONTRIBUTING.md
  5. 1 1
      packages/ckeditor5-editor-classic/LICENSE.md
  6. 1 2
      packages/ckeditor5-editor-classic/README.md
  7. 23 20
      packages/ckeditor5-editor-classic/package.json
  8. 73 71
      packages/ckeditor5-editor-classic/src/classiceditor.js
  9. 131 16
      packages/ckeditor5-editor-classic/src/classiceditorui.js
  10. 5 11
      packages/ckeditor5-editor-classic/src/classiceditoruiview.js
  11. 71 54
      packages/ckeditor5-editor-classic/tests/classiceditor.js
  12. 151 32
      packages/ckeditor5-editor-classic/tests/classiceditorui.js
  13. 13 19
      packages/ckeditor5-editor-classic/tests/classiceditoruiview.js
  14. 4 4
      packages/ckeditor5-editor-classic/tests/manual/classiceditor-data.js
  15. 4 4
      packages/ckeditor5-editor-classic/tests/manual/classiceditor.js
  16. 20 0
      packages/ckeditor5-editor-classic/tests/manual/memory.html
  17. 71 0
      packages/ckeditor5-editor-classic/tests/manual/memory.js
  18. 31 0
      packages/ckeditor5-editor-classic/tests/manual/memory.md
  19. 5 0
      packages/ckeditor5-editor-classic/tests/manual/placeholder.html
  20. 37 0
      packages/ckeditor5-editor-classic/tests/manual/placeholder.js
  21. 8 0
      packages/ckeditor5-editor-classic/tests/manual/placeholder.md
  22. BIN
      packages/ckeditor5-editor-classic/tests/manual/sample.jpg
  23. 2 2
      packages/ckeditor5-editor-classic/tests/manual/tickets/60/1.js
  24. 2 2
      packages/ckeditor5-editor-classic/theme/classiceditor.css

+ 2 - 2
packages/ckeditor5-editor-classic/.eslintrc.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /* eslint-env node */

+ 11 - 13
packages/ckeditor5-editor-classic/.travis.yml

@@ -1,33 +1,31 @@
 sudo: required
 dist: trusty
 addons:
+  chrome: stable
   firefox: latest
-  apt:
-    sources:
-    - google-chrome
-    packages:
-    - google-chrome-stable
 language: node_js
 node_js:
 - '8'
 cache:
-- node_modules
+  yarn: true
 branches:
-   except:
-   - stable
+  except:
+  - stable
 before_install:
+- export START_TIME=$( date +%s )
 - export DISPLAY=:99.0
 - sh -e /etc/init.d/xvfb start
-- npm config set package-lock false
-- npm i -g npm@^5.7.1
+- npm i -g yarn
 install:
-- npm install @ckeditor/ckeditor5-dev-tests
+- yarn add @ckeditor/ckeditor5-dev-tests
 - ckeditor5-dev-tests-install-dependencies
 script:
 - ckeditor5-dev-tests-travis
 after_success:
 - ckeditor5-dev-tests-save-revision
+after_script:
+- export END_TIME=$( date +%s )
+- ckeditor5-dev-tests-notify-travis-status
 env:
   global:
-  - secure: yGjqvN+FNsbRuAneBgIi9AyraRbhCSm8MEd7FPq/exSTjfsWofOq5i9O7o7/02bJVZ8rveD71OaqAKQCWVG8BD4umbpu0N0PpkJbjVi8jmytzN28mUxgrmZkFbr96C5YnGqvdxf4AB9wT5ExtzTfp5XUO383/WoGPu9qt6rmb5II1PHwhT1WzXql/pCU5eGLccCYtlEr3ybWD9n3wiC4fM90M5axuSu9EwpvTEfMJQbeqEv7jSu+qlPUAJ4rSTmgt1pGIMxJgUpDi76qnn4VdTxRUZy+4heAFT9c/96tPV0QAz8cpVM3qGKSTuF8FAY71sddF4PX8wfG/1GjO+dHEmbop5Kz8UXnS8rnp9gx4Q5be8iW0DfPCqjc8a2ULWgUMr4jBnbZKpMHNT83Li1TfGflIiDksh68EKMRHHoVzxKJ2ysBSMzPqLfLXSyY9Jl/j5OBAiVl1n22oq5t9AlT6JEm0bYcgIwVIRpJqPMZMkZBCoLZHSyVfdIqdT+bcNgSEG5iLrXS9FlqsOS9FB4U3DbvPZbNBq+uoEuAyk5w+T5OkqIhXBddUG9emaa6TuQNNcATyucwWxB3HC1UAK8s5XTWVUzQ86nUShzH7aHJky+PJN0hkYAxwjldwtzTWwKdw+SBjk3Cl5h8JEepJSX0Lqtwci4XGoHsJ4JL5UCU5OM=
-  - secure: KDj0ramEp45sMIyRogmNesjH5O2q7k5kopvDjG5+O1PWjMc+moxoSLYdcqknINLdHkid/SfwyfDppO6rvsVuLR9nvq7SPsM/xDGtLMoLYwUoWPrK57tQ1exl1FUWHzZs/7UJOwC+GnqqtL6bN6PAAj4aSLoTai/xDiDBTRMrVzDmYGeHaHihI44jtqsfBEkCGd9vfB0JDZvhncZqQrEubTwFnh+QwZXTnVR837p+EWhT87HvH/OPioXPnEF3O3N72u/WpqmQKrgM2vUWOfjHzZTEleBBJZnbrFFKzoOczig2a4I0hLiaqtK/bsSpjMpyKVzW+Jh+bQiKLk2B3DlUL+/6RH/dioo0DM438dI5sT46dNDl8k3mXCfJyGpn1by3EKdjEkkgLz095YzaCIEKBLVqKky4FhqRduEGpmXXMJw5xJQuaLOqC8jg0bgAl/E6q0uMyVPJuaC1ewGOHSx33j09ZXrOCAk57KzXgJzVi3BJR2kf4x2KygRqqVtFVYjOyvdAS2EieytMKwMVfQfrtmgJU5KCcbYzNX6ZEKEdS7SxTlyPtDdJssxvuuRU8xxgkswBxTkFGFG77dy0BTsa0xmAdiJnb/Z3F9hJP3VySOKj/lnATO3Yzr5v5l0jD1SQqfG9IYvDE/fDULs7/cFuj46voHIVBkziQ+IAqv/3vYo=
+  - secure: WPoK5kuljD9HmH7+aA7d20+FZV87th3OZCDye3p3iX4vKalv16YtCzdPyRslBJZGLyY1thn6r7rJAM9h8nmpQbSi4QTKrVw7WSIpEDkXv4iPlnVY8ovZbRwifl1X/piTW+xim6C9Hv4Nd7fslfHxKg/8BTS2Lt7bVChRY2unfTGCidu0JZzNt7N6iH6R9cmt1ahB8s9cVu5E9DdqaTu7YlTMu2f+oEjjp3/NzYk+J8IBU4F9ffGU+KoYBhphOjoq6BAm2sBGpzDDLQxXKxwe1Wa4ORFU2m9OwRhttlmyKYPAXDelwPtfcJAu4FLhtwlYO8qzP56ok1k9mf7dveaY9STGFtAcq7c9UiKIKvHQd+JAzbIZ9enqKPkgOJFEQ/6tzHKBF1tK3gEyKxWtxBuCmF0aZpd2aMwgiGOuQcljHMz7pl6+otVIw1yG9yqDRzRZe/1N024AD+CRn5klkfEWGNeZE1lvHBTi4612wqR8RwCy7844bEMrddDYkfZsYDXoyS3s8dV4C9SD6ajFwI5u6xkPm2jIybHziwQVHIUiNDdyEfwGGsjRb9cm3wbdK08TfGhbF5wgMlYfGgv3nukIB+Bh+hXIdO4Kv0QE9AiNnRKzX9kAB3gwdSw4Km/bg9aC7+lxdsQ7Ncn96HDM+OyT303G2kE0W4LHdt3EOaSHqq0=

+ 51 - 0
packages/ckeditor5-editor-classic/CHANGELOG.md

@@ -1,6 +1,57 @@
 Changelog
 =========
 
+## [12.1.3](https://github.com/ckeditor/ckeditor5-editor-classic/compare/v12.1.2...v12.1.3) (2019-07-10)
+
+Internal changes only (updated dependencies, documentation, etc.).
+
+
+## [12.1.2](https://github.com/ckeditor/ckeditor5-editor-classic/compare/v12.1.1...v12.1.2) (2019-07-04)
+
+Internal changes only (updated dependencies, documentation, etc.).
+
+
+## [12.1.1](https://github.com/ckeditor/ckeditor5-editor-classic/compare/v12.1.0...v12.1.1) (2019-06-05)
+
+Internal changes only (updated dependencies, documentation, etc.).
+
+
+## [12.1.0](https://github.com/ckeditor/ckeditor5-editor-classic/compare/v12.0.0...v12.1.0) (2019-04-10)
+
+### Features
+
+* Introduced `EditorConfig#initialData`. ([fce3edc](https://github.com/ckeditor/ckeditor5-editor-classic/commit/fce3edc))
+
+
+## [12.0.0](https://github.com/ckeditor/ckeditor5-editor-classic/compare/v11.0.2...v12.0.0) (2019-02-28)
+
+### Features
+
+* Added support for the `config.placeholder` option which allows configuring the empty editor content placeholder (see [ckeditor/ckeditor5#479](https://github.com/ckeditor/ckeditor5/issues/479)). ([3450c23](https://github.com/ckeditor/ckeditor5-editor-classic/commit/3450c23))
+
+### Bug fixes
+
+* Fixed memory leaks during editor initialization and destruction (see [ckeditor/ckeditor5#1341](https://github.com/ckeditor/ckeditor5/issues/1341)). ([a3c5c82](https://github.com/ckeditor/ckeditor5-editor-classic/commit/a3c5c82))
+
+### Other changes
+
+* Adjustments to new editor initialization events. See breaking changes. ([61ccab0](https://github.com/ckeditor/ckeditor5-editor-classic/commit/61ccab0))
+* Editor UI classes API refactoring. See breaking changes. ([74e27ae](https://github.com/ckeditor/ckeditor5-editor-classic/commit/74e27ae))
+
+### BREAKING CHANGES
+
+* Upgraded minimal versions of Node to `8.0.0` and npm to `5.7.1`. See: [ckeditor/ckeditor5#1507](https://github.com/ckeditor/ckeditor5/issues/1507). ([612ea3c](https://github.com/ckeditor/ckeditor5-cloud-services/commit/612ea3c))
+* The `editor#dataReady` event was removed. The `editor.data#ready` event has been introduced and should be used instead.
+* The `editor#pluginsReady` event was removed. Use plugin `afterInit()` method instead.
+* Removed `ClassicEditor#element` property. The `ClassicEditorUI#element` property should be used instead.
+* Removed `ClassicEditorUIView#editableElement`. Instead `ClassicEditorUI#getEditableElement()` method should be used.
+
+
+## [11.0.2](https://github.com/ckeditor/ckeditor5-editor-classic/compare/v11.0.1...v11.0.2) (2018-12-05)
+
+Internal changes only (updated dependencies, documentation, etc.).
+
+
 ## [11.0.1](https://github.com/ckeditor/ckeditor5-editor-classic/compare/v11.0.0...v11.0.1) (2018-10-08)
 
 Internal changes only (updated dependencies, documentation, etc.).

+ 1 - 1
packages/ckeditor5-editor-classic/CONTRIBUTING.md

@@ -1,4 +1,4 @@
 Contributing
 ========================================
 
-Information about contributing can be found on the following page: <https://github.com/ckeditor/ckeditor5/blob/master/CONTRIBUTING.md>.
+See the [official contributors' guide to CKEditor 5](https://ckeditor.com/docs/ckeditor5/latest/framework/guides/contributing/contributing.html) to learn more.

+ 1 - 1
packages/ckeditor5-editor-classic/LICENSE.md

@@ -2,7 +2,7 @@ Software License Agreement
 ==========================
 
 **Classic Editor** – https://github.com/ckeditor/ckeditor5-editor-classic <br>
-Copyright (c) 2003-2018, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.
+Copyright (c) 2003-2019, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.
 
 Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html).
 

+ 1 - 2
packages/ckeditor5-editor-classic/README.md

@@ -4,7 +4,6 @@ CKEditor 5 classic editor implementation
 [![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-editor-classic.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-editor-classic)
 [![Build Status](https://travis-ci.org/ckeditor/ckeditor5-editor-classic.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-editor-classic)
-[![BrowserStack Status](https://automate.browserstack.com/automate/badge.svg?badge_key=d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)](https://automate.browserstack.com/public-build/d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)
 [![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5-editor-classic/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5-editor-classic?branch=master)
 <br>
 [![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-editor-classic/status.svg)](https://david-dm.org/ckeditor/ckeditor5-editor-classic)
@@ -22,4 +21,4 @@ See the [`@ckeditor/ckeditor5-editor-classic` package](https://ckeditor.com/docs
 
 ## License
 
-Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html). For full details about the license, please check the `LICENSE.md` file.
+Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html). For full details about the license, please check the `LICENSE.md` file or [https://ckeditor.com/legal/ckeditor-oss-license](https://ckeditor.com/legal/ckeditor-oss-license).

+ 23 - 20
packages/ckeditor5-editor-classic/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@ckeditor/ckeditor5-editor-classic",
-  "version": "11.0.1",
+  "version": "12.1.3",
   "description": "Classic editor implementation for CKEditor 5.",
   "keywords": [
     "ckeditor",
@@ -9,32 +9,31 @@
     "ckeditor5-editor"
   ],
   "dependencies": {
-    "@ckeditor/ckeditor5-core": "^11.0.1",
-    "@ckeditor/ckeditor5-engine": "^11.0.0",
-    "@ckeditor/ckeditor5-theme-lark": "^11.1.0",
-    "@ckeditor/ckeditor5-ui": "^11.1.0",
-    "@ckeditor/ckeditor5-utils": "^11.0.0",
+    "@ckeditor/ckeditor5-core": "^12.2.1",
+    "@ckeditor/ckeditor5-engine": "^13.2.1",
+    "@ckeditor/ckeditor5-ui": "^13.0.2",
+    "@ckeditor/ckeditor5-utils": "^13.0.1",
     "lodash-es": "^4.17.10"
   },
   "devDependencies": {
-    "@ckeditor/ckeditor5-basic-styles": "^10.0.3",
-    "@ckeditor/ckeditor5-enter": "^10.1.2",
-    "@ckeditor/ckeditor5-heading": "^10.1.0",
-    "@ckeditor/ckeditor5-paragraph": "^10.0.3",
-    "@ckeditor/ckeditor5-typing": "^11.0.1",
-    "@ckeditor/ckeditor5-undo": "^10.0.3",
+    "@ckeditor/ckeditor5-basic-styles": "^11.1.3",
+    "@ckeditor/ckeditor5-enter": "^11.0.4",
+    "@ckeditor/ckeditor5-heading": "^11.0.4",
+    "@ckeditor/ckeditor5-paragraph": "^11.0.4",
+    "@ckeditor/ckeditor5-typing": "^12.1.1",
+    "@ckeditor/ckeditor5-undo": "^11.0.4",
     "eslint": "^5.5.0",
-    "eslint-config-ckeditor5": "^1.0.7",
-    "husky": "^0.14.3",
+    "eslint-config-ckeditor5": "^2.0.0",
+    "husky": "^1.3.1",
     "lint-staged": "^7.0.0"
   },
   "engines": {
-    "node": ">=6.9.0",
-    "npm": ">=3.0.0"
+    "node": ">=8.0.0",
+    "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",
   "license": "GPL-2.0-or-later",
-  "homepage": "https://ckeditor.com",
+  "homepage": "https://ckeditor.com/ckeditor-5",
   "bugs": "https://github.com/ckeditor/ckeditor5-editor-classic/issues",
   "repository": {
     "type": "git",
@@ -46,8 +45,7 @@
     "theme"
   ],
   "scripts": {
-    "lint": "eslint --quiet '**/*.js'",
-    "precommit": "lint-staged"
+    "lint": "eslint --quiet '**/*.js'"
   },
   "lint-staged": {
     "**/*.js": [
@@ -57,5 +55,10 @@
   "eslintIgnore": [
     "src/lib/**",
     "packages/**"
-  ]
+  ],
+  "husky": {
+    "hooks": {
+      "pre-commit": "lint-staged"
+    }
+  }
 }

+ 73 - 71
packages/ckeditor5-editor-classic/src/classiceditor.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -14,10 +14,10 @@ import attachToForm from '@ckeditor/ckeditor5-core/src/editor/utils/attachtoform
 import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
 import ClassicEditorUI from './classiceditorui';
 import ClassicEditorUIView from './classiceditoruiview';
-import ElementReplacer from '@ckeditor/ckeditor5-utils/src/elementreplacer';
 import getDataFromElement from '@ckeditor/ckeditor5-utils/src/dom/getdatafromelement';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import { isElement } from 'lodash-es';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
  * The {@glink builds/guides/overview#classic-editor classic editor} implementation.
@@ -66,30 +66,15 @@ export default class ClassicEditor extends Editor {
 			this.sourceElement = sourceElementOrData;
 		}
 
-		/**
-		 * The element replacer instance used to hide the editor's source element.
-		 *
-		 * @protected
-		 * @member {module:utils/elementreplacer~ElementReplacer}
-		 */
-		this._elementReplacer = new ElementReplacer();
-
 		this.data.processor = new HtmlDataProcessor();
 
 		this.model.document.createRoot();
 
-		this.ui = new ClassicEditorUI( this, new ClassicEditorUIView( this.locale ) );
+		this.ui = new ClassicEditorUI( this, new ClassicEditorUIView( this.locale, this.editing.view ) );
 
 		attachToForm( this );
 	}
 
-	/**
-	 * @inheritDoc
-	 */
-	get element() {
-		return this.ui.view.element;
-	}
-
 	/**
 	 * Destroys the editor instance, releasing all resources used by it.
 	 *
@@ -102,16 +87,19 @@ export default class ClassicEditor extends Editor {
 			this.updateSourceElement();
 		}
 
-		this._elementReplacer.restore();
 		this.ui.destroy();
 
 		return super.destroy();
 	}
 
 	/**
-	 * Creates a classic editor instance.
+	 * Creates a new classic editor instance.
+	 *
+	 * There are three ways how the editor can be initialized.
 	 *
-	 * Creating an instance when using a {@glink builds/index CKEditor build}:
+	 * # Replacing a DOM element (and loading data from it)
+	 *
+	 * You can initialize the editor using an existing DOM element:
 	 *
 	 *		ClassicEditor
 	 *			.create( document.querySelector( '#editor' ) )
@@ -122,93 +110,103 @@ export default class ClassicEditor extends Editor {
 	 *				console.error( err.stack );
 	 *			} );
 	 *
-	 * Creating an instance when using CKEditor from source (make sure to specify the list of plugins to load and the toolbar):
+	 * The element's content will be used as the editor data and the element will be replaced by the editor UI.
+	 *
+	 * # Creating a detached editor
 	 *
-	 *		import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
-	 *		import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
-	 *		import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
-	 *		import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
-	 *		import ...
+	 * Alternatively, you can initialize the editor by passing the initial data directly as a string.
+	 * In this case, the editor will render an element that must be inserted into the DOM:
 	 *
 	 *		ClassicEditor
-	 *			.create( document.querySelector( '#editor' ), {
-	 *				plugins: [ Essentials, Bold, Italic, ... ],
-	 *				toolbar: [ 'bold', 'italic', ... ]
-	 *			} )
+	 *			.create( '<p>Hello world!</p>' )
 	 *			.then( editor => {
 	 *				console.log( 'Editor was initialized', editor );
+	 *
+	 *				// Initial data was provided so the editor UI element needs to be added manually to the DOM.
+	 *				document.body.appendChild( editor.ui.element );
 	 *			} )
 	 *			.catch( err => {
 	 *				console.error( err.stack );
 	 *			} );
 	 *
-	 * Creating an instance when using initial data instead of a DOM element:
+	 * This lets you dynamically append the editor to your web page whenever it is convenient for you. You may use this method if your
+	 * web page content is generated on the client side and the DOM structure is not ready at the moment when you initialize the editor.
 	 *
-	 *		import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
-	 *		import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
-	 *		import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
-	 *		import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
-	 *		import ...
+	 * # Replacing a DOM element (and data provided in `config.initialData`)
+	 *
+	 * You can also mix these two ways by providing a DOM element to be used and passing the initial data through the configuration:
 	 *
 	 *		ClassicEditor
-	 *			.create( '<p>Hello world!</p>' )
+	 *			.create( document.querySelector( '#editor' ), {
+	 *				initialData: '<h2>Initial data</h2><p>Foo bar.</p>'
+	 *			} )
 	 *			.then( editor => {
 	 *				console.log( 'Editor was initialized', editor );
-	 *
-	 *				// Initial data was provided so `editor.element` needs to be added manually to the DOM.
-	 *				document.body.appendChild( editor.element );
 	 *			} )
 	 *			.catch( err => {
 	 *				console.error( err.stack );
 	 *			} );
 	 *
+	 * This method can be used to initialize the editor on an existing element with the specified content in case if your integration
+	 * makes it difficult to set the content of the source element.
+	 *
+	 * Note that an error will be thrown if you pass the initial data both as the first parameter and also in the configuration.
+	 *
+	 * # Configuring the editor
+	 *
+	 * See the {@link module:core/editor/editorconfig~EditorConfig editor configuration documentation} to learn more about
+	 * customizing plugins, toolbar and more.
+	 *
+	 * # Using the editor from source
+	 *
+	 * The code samples listed in the previous sections of this documentation assume that you are using an
+	 * {@glink builds/guides/overview editor build} (for example – `@ckeditor/ckeditor5-build-classic`).
+	 *
+	 * If you want to use the classic editor from source (`@ckeditor/ckeditor5-editor-classic/src/classiceditor`),
+	 * you need to define the list of
+	 * {@link module:core/editor/editorconfig~EditorConfig#plugins plugins to be initialized} and
+	 * {@link module:core/editor/editorconfig~EditorConfig#toolbar toolbar items}. Read more about using the editor from
+	 * source in the {@glink builds/guides/integration/advanced-setup "Advanced setup" guide}.
+	 *
 	 * @param {HTMLElement|String} sourceElementOrData The DOM element that will be the source for the created editor
 	 * or the editor's initial data.
 	 *
-	 * If a source element is passed, then its contents will be automatically
-	 * {@link module:editor-classic/classiceditor~ClassicEditor#setData loaded} to the editor on startup
-	 * and the {@link module:core/editor/editorwithui~EditorWithUI#element editor element} will replace the passed element in the DOM
-	 * (the original one will be hidden and the editor will be injected next to it).
+	 * If a DOM element is passed, its content will be automatically loaded to the editor upon initialization
+	 * and the {@link module:editor-classic/classiceditorui~ClassicEditorUI#element editor element} will replace the passed element
+	 * in the DOM (the original one will be hidden and the editor will be injected next to it).
 	 *
-	 * Moreover, the data will be set back to the source element once the editor is destroyed and
-	 * (if the element is a `<textarea>`) when a form in which this element is contained is submitted (which ensures
-	 * automatic integration with native web forms).
+	 * Moreover, the editor data will be set back to the original element once the editor is destroyed and when a form, in which
+	 * this element is contained, is submitted (if the original element is a `<textarea>`). This ensures seamless integration with native
+	 * web forms.
 	 *
-	 * If the data is passed, a detached editor will be created. It means that you need to insert it into the DOM manually
-	 * (by accessing the {@link module:editor-classic/classiceditor~ClassicEditor#element `editor.element`} property).
+	 * If the initial data is passed, a detached editor will be created. In this case you need to insert it into the DOM manually.
+	 * It is available under the {@link module:editor-classic/classiceditorui~ClassicEditorUI#element `editor.ui.element`} property.
 	 *
-	 * See the examples above to learn more.
-	 *
-	 * @param {module:core/editor/editorconfig~EditorConfig} config The editor configuration.
-	 * @returns {Promise} A promise resolved once the editor is ready.
-	 * The promise returns the created {@link module:editor-classic/classiceditor~ClassicEditor} instance.
+	 * @param {module:core/editor/editorconfig~EditorConfig} [config] The editor configuration.
+	 * @returns {Promise} A promise resolved once the editor is ready. The promise resolves with the created editor instance.
 	 */
-	static create( sourceElementOrData, config ) {
+	static create( sourceElementOrData, config = {} ) {
 		return new Promise( resolve => {
 			const editor = new this( sourceElementOrData, config );
 
 			resolve(
 				editor.initPlugins()
-					.then( () => editor.ui.init() )
+					.then( () => editor.ui.init( isElement( sourceElementOrData ) ? sourceElementOrData : null ) )
 					.then( () => {
-						if ( isElement( sourceElementOrData ) ) {
-							editor._elementReplacer.replace( sourceElementOrData, editor.element );
+						if ( !isElement( sourceElementOrData ) && config.initialData ) {
+							// Documented in core/editor/editorconfig.jdoc.
+							throw new CKEditorError(
+								'editor-create-initial-data: ' +
+								'The config.initialData option cannot be used together with initial data passed in Editor.create().',
+								null
+							);
 						}
 
-						editor.fire( 'uiReady' );
-					} )
-					.then( () => editor.editing.view.attachDomRoot( editor.ui.view.editableElement ) )
-					.then( () => {
-						const initialData = isElement( sourceElementOrData ) ?
-							getDataFromElement( sourceElementOrData ) :
-							sourceElementOrData;
+						const initialData = config.initialData || getInitialData( sourceElementOrData );
 
 						return editor.data.init( initialData );
 					} )
-					.then( () => {
-						editor.fire( 'dataReady' );
-						editor.fire( 'ready' );
-					} )
+					.then( () => editor.fire( 'ready' ) )
 					.then( () => editor )
 			);
 		} );
@@ -217,3 +215,7 @@ export default class ClassicEditor extends Editor {
 
 mix( ClassicEditor, DataApiMixin );
 mix( ClassicEditor, ElementApiMixin );
+
+function getInitialData( sourceElementOrData ) {
+	return isElement( sourceElementOrData ) ? getDataFromElement( sourceElementOrData ) : sourceElementOrData;
+}

+ 131 - 16
packages/ckeditor5-editor-classic/src/classiceditorui.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -10,6 +10,8 @@
 import EditorUI from '@ckeditor/ckeditor5-core/src/editor/editorui';
 import enableToolbarKeyboardFocus from '@ckeditor/ckeditor5-ui/src/toolbar/enabletoolbarkeyboardfocus';
 import normalizeToolbarConfig from '@ckeditor/ckeditor5-ui/src/toolbar/normalizetoolbarconfig';
+import { enablePlaceholder } from '@ckeditor/ckeditor5-engine/src/view/placeholder';
+import ElementReplacer from '@ckeditor/ckeditor5-utils/src/elementreplacer';
 
 /**
  * The classic editor UI class.
@@ -18,29 +20,126 @@ import normalizeToolbarConfig from '@ckeditor/ckeditor5-ui/src/toolbar/normalize
  */
 export default class ClassicEditorUI extends EditorUI {
 	/**
-	 * @inheritDoc
+	 * Creates an instance of the classic editor UI class.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor The editor instance.
+	 * @param {module:ui/editorui/editoruiview~EditorUIView} view The view of the UI.
 	 */
 	constructor( editor, view ) {
-		super( editor, view );
+		super( editor );
+
+		/**
+		 * The main (top–most) view of the editor UI.
+		 *
+		 * @readonly
+		 * @member {module:ui/editorui/editoruiview~EditorUIView} #view
+		 */
+		this.view = view;
 
 		/**
 		 * A normalized `config.toolbar` object.
 		 *
-		 * @type {Object}
 		 * @private
+		 * @member {Object}
 		 */
 		this._toolbarConfig = normalizeToolbarConfig( editor.config.get( 'toolbar' ) );
+
+		/**
+		 * The element replacer instance used to hide the editor's source element.
+		 *
+		 * @protected
+		 * @member {module:utils/elementreplacer~ElementReplacer}
+		 */
+		this._elementReplacer = new ElementReplacer();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	get element() {
+		return this.view.element;
 	}
 
 	/**
 	 * Initializes the UI.
+	 *
+	 * @param {HTMLElement|null} replacementElement The DOM element that will be the source for the created editor.
 	 */
-	init() {
+	init( replacementElement ) {
 		const editor = this.editor;
 		const view = this.view;
+		const editingView = editor.editing.view;
+		const editable = view.editable;
+		const editingRoot = editingView.document.getRoot();
+
+		// The editable UI and editing root should share the same name. Then name is used
+		// to recognize the particular editable, for instance in ARIA attributes.
+		editable.name = editingRoot.rootName;
 
 		view.render();
 
+		// The editable UI element in DOM is available for sure only after the editor UI view has been rendered.
+		// But it can be available earlier if a DOM element has been passed to BalloonEditor.create().
+		const editableElement = editable.element;
+
+		// Register the editable UI view in the editor. A single editor instance can aggregate multiple
+		// editable areas (roots) but the classic editor has only one.
+		this.setEditableElement( editable.name, editableElement );
+
+		// Let the global focus tracker know that the editable UI element is focusable and
+		// belongs to the editor. From now on, the focus tracker will sustain the editor focus
+		// as long as the editable is focused (e.g. the user is typing).
+		this.focusTracker.add( editableElement );
+
+		// Let the editable UI element respond to the changes in the global editor focus
+		// tracker. It has been added to the same tracker a few lines above but, in reality, there are
+		// many focusable areas in the editor, like balloons, toolbars or dropdowns and as long
+		// as they have focus, the editable should act like it is focused too (although technically
+		// it isn't), e.g. by setting the proper CSS class, visually announcing focus to the user.
+		// Doing otherwise will result in editable focus styles disappearing, once e.g. the
+		// toolbar gets focused.
+		view.editable.bind( 'isFocused' ).to( this.focusTracker );
+
+		// Bind the editable UI element to the editing view, making it an end– and entry–point
+		// of the editor's engine. This is where the engine meets the UI.
+		editingView.attachDomRoot( editableElement );
+
+		// If an element containing the initial data of the editor was provided, replace it with
+		// an editor instance's UI in DOM until the editor is destroyed. For instance, a <textarea>
+		// can be such element.
+		if ( replacementElement ) {
+			this._elementReplacer.replace( replacementElement, this.element );
+		}
+
+		this._initPlaceholder();
+		this._initToolbar();
+		this.fire( 'ready' );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	destroy() {
+		const view = this.view;
+		const editingView = this.editor.editing.view;
+
+		this._elementReplacer.restore();
+		editingView.detachDomRoot( view.editable.name );
+		view.destroy();
+
+		super.destroy();
+	}
+
+	/**
+	 * Initializes the editor toolbar.
+	 *
+	 * @private
+	 */
+	_initToolbar() {
+		const editor = this.editor;
+		const view = this.view;
+		const editingView = editor.editing.view;
+
 		// Set–up the sticky panel with toolbar.
 		view.stickyPanel.bind( 'isActive' ).to( this.focusTracker, 'isFocused' );
 		view.stickyPanel.limiterElement = view.element;
@@ -49,14 +148,6 @@ export default class ClassicEditorUI extends EditorUI {
 			view.stickyPanel.viewportTopOffset = this._toolbarConfig.viewportTopOffset;
 		}
 
-		// Setup the editable.
-		const editingRoot = editor.editing.view.document.getRoot();
-		view.editable.bind( 'isReadOnly' ).to( editingRoot );
-		view.editable.bind( 'isFocused' ).to( editor.editing.view.document );
-		view.editable.name = editingRoot.rootName;
-
-		this.focusTracker.add( this.view.editableElement );
-
 		view.toolbar.fillFromConfig( this._toolbarConfig.items, this.componentFactory );
 
 		editor.on( 'ready', () => {
@@ -70,10 +161,34 @@ export default class ClassicEditorUI extends EditorUI {
 		} );
 
 		enableToolbarKeyboardFocus( {
-			origin: editor.editing.view,
+			origin: editingView,
 			originFocusTracker: this.focusTracker,
 			originKeystrokeHandler: editor.keystrokes,
-			toolbar: this.view.toolbar
+			toolbar: view.toolbar
 		} );
 	}
+
+	/**
+	 * Enable the placeholder text on the editing root, if any was configured.
+	 *
+	 * @private
+	 */
+	_initPlaceholder() {
+		const editor = this.editor;
+		const editingView = editor.editing.view;
+		const editingRoot = editingView.document.getRoot();
+		const sourceElement = editor.sourceElement;
+
+		const placeholderText = editor.config.get( 'placeholder' ) ||
+			sourceElement && sourceElement.tagName.toLowerCase() === 'textarea' && sourceElement.getAttribute( 'placeholder' );
+
+		if ( placeholderText ) {
+			enablePlaceholder( {
+				view: editingView,
+				element: editingRoot,
+				text: placeholderText,
+				isDirectHost: false
+			} );
+		}
+	}
 }

+ 5 - 11
packages/ckeditor5-editor-classic/src/classiceditoruiview.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -25,8 +25,9 @@ export default class ClassicEditorUIView extends BoxedEditorUIView {
 	 * Creates an instance of the classic editor UI view.
 	 *
 	 * @param {module:utils/locale~Locale} locale The {@link module:core/editor/editor~Editor#locale} instance.
+	 * @param {module:engine/view/view~View} editingView The editing view instance this view is related to.
 	 */
-	constructor( locale ) {
+	constructor( locale, editingView ) {
 		super( locale );
 
 		/**
@@ -52,7 +53,7 @@ export default class ClassicEditorUIView extends BoxedEditorUIView {
 		 * @readonly
 		 * @member {module:ui/editableui/inline/inlineeditableuiview~InlineEditableUIView}
 		 */
-		this.editable = new InlineEditableUIView( locale );
+		this.editable = new InlineEditableUIView( locale, editingView );
 	}
 
 	/**
@@ -67,11 +68,4 @@ export default class ClassicEditorUIView extends BoxedEditorUIView {
 		this.top.add( this.stickyPanel );
 		this.main.add( this.editable );
 	}
-
-	/**
-	 * @inheritDoc
-	 */
-	get editableElement() {
-		return this.editable.element;
-	}
 }

+ 71 - 54
packages/ckeditor5-editor-classic/tests/classiceditor.js

@@ -1,9 +1,9 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-/* globals document, Event */
+/* globals document, Event, console */
 
 import ClassicEditorUI from '../src/classiceditorui';
 import ClassicEditorUIView from '../src/classiceditoruiview';
@@ -20,6 +20,9 @@ import RootElement from '@ckeditor/ckeditor5-engine/src/model/rootelement';
 
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+import { describeMemoryUsage, testMemoryUsage } from '@ckeditor/ckeditor5-core/tests/_utils/memory';
+
 describe( 'ClassicEditor', () => {
 	let editor, editorElement;
 
@@ -30,6 +33,8 @@ describe( 'ClassicEditor', () => {
 		editorElement.innerHTML = '<p><strong>foo</strong> bar</p>';
 
 		document.body.appendChild( editorElement );
+
+		testUtils.sinon.stub( console, 'warn' ).callsFake( () => {} );
 	} );
 
 	afterEach( () => {
@@ -92,14 +97,6 @@ describe( 'ClassicEditor', () => {
 			} );
 		} );
 
-		it( 'allows to pass data to the constructor', () => {
-			return ClassicEditor.create( '<p>Hello world!</p>', {
-				plugins: [ Paragraph ]
-			} ).then( editor => {
-				expect( editor.getData() ).to.equal( '<p>Hello world!</p>' );
-			} );
-		} );
-
 		describe( 'ui', () => {
 			it( 'creates the UI using BoxedEditorUI classes', () => {
 				expect( editor.ui ).to.be.instanceof( ClassicEditorUI );
@@ -149,6 +146,49 @@ describe( 'ClassicEditor', () => {
 				} );
 		} );
 
+		it( 'should not require config object', () => {
+			// Just being safe with `builtinPlugins` static property.
+			class CustomClassicEditor extends ClassicEditor {}
+			CustomClassicEditor.builtinPlugins = [ Paragraph, Bold ];
+
+			return CustomClassicEditor.create( editorElement )
+				.then( newEditor => {
+					expect( newEditor.getData() ).to.equal( '<p><strong>foo</strong> bar</p>' );
+
+					return newEditor.destroy();
+				} );
+		} );
+
+		it( 'allows to pass data to the constructor', () => {
+			return ClassicEditor.create( '<p>Hello world!</p>', {
+				plugins: [ Paragraph ]
+			} ).then( editor => {
+				expect( editor.getData() ).to.equal( '<p>Hello world!</p>' );
+
+				editor.destroy();
+			} );
+		} );
+
+		it( 'initializes with config.initialData', () => {
+			return ClassicEditor.create( editorElement, {
+				initialData: '<p>Hello world!</p>',
+				plugins: [ Paragraph ]
+			} ).then( editor => {
+				expect( editor.getData() ).to.equal( '<p>Hello world!</p>' );
+
+				editor.destroy();
+			} );
+		} );
+
+		it( 'throws if initial data is passed in Editor#create and config.initialData is also used', done => {
+			ClassicEditor.create( '<p>Hello world!</p>', {
+				initialData: '<p>I am evil!</p>',
+				plugins: [ Paragraph ]
+			} ).catch( () => {
+				done();
+			} );
+		} );
+
 		it( 'should have undefined the #sourceElement if editor was initialized with data', () => {
 			return ClassicEditor
 				.create( '<p>Foo.</p>', {
@@ -169,20 +209,6 @@ describe( 'ClassicEditor', () => {
 			it( 'attaches editable UI as view\'s DOM root', () => {
 				expect( editor.editing.view.getDomRoot() ).to.equal( editor.ui.view.editable.element );
 			} );
-
-			it( 'editor.element points to the editor\'s UI when editor was initialized on the DOM element', () => {
-				expect( editor.element ).to.equal( editor.ui.view.element );
-			} );
-
-			it( 'editor.element points to the editor\'s UI when editor was initialized with data', () => {
-				return ClassicEditor.create( '<p>Hello world!</p>', {
-					plugins: [ Paragraph ]
-				} ).then( editor => {
-					expect( editor.element ).to.equal( editor.ui.view.element );
-
-					return editor.destroy();
-				} );
-			} );
 		} );
 	} );
 
@@ -195,14 +221,13 @@ describe( 'ClassicEditor', () => {
 			const fired = [];
 
 			function spy( evt ) {
-				fired.push( evt.name );
+				fired.push( `${ evt.name }-${ evt.source.constructor.name.toLowerCase() }` );
 			}
 
 			class EventWatcher extends Plugin {
 				init() {
-					this.editor.on( 'pluginsReady', spy );
-					this.editor.on( 'uiReady', spy );
-					this.editor.on( 'dataReady', spy );
+					this.editor.ui.on( 'ready', spy );
+					this.editor.data.on( 'ready', spy );
 					this.editor.on( 'ready', spy );
 				}
 			}
@@ -212,40 +237,19 @@ describe( 'ClassicEditor', () => {
 					plugins: [ EventWatcher ]
 				} )
 				.then( newEditor => {
-					expect( fired ).to.deep.equal( [ 'pluginsReady', 'uiReady', 'dataReady', 'ready' ] );
-
-					editor = newEditor;
-				} );
-		} );
-
-		it( 'fires dataReady once data is loaded', () => {
-			let data;
-
-			class EventWatcher extends Plugin {
-				init() {
-					this.editor.on( 'dataReady', () => {
-						data = this.editor.getData();
-					} );
-				}
-			}
-
-			return ClassicEditor
-				.create( editorElement, {
-					plugins: [ EventWatcher, Paragraph, Bold ]
-				} )
-				.then( newEditor => {
-					expect( data ).to.equal( '<p><strong>foo</strong> bar</p>' );
+					expect( fired ).to.deep.equal(
+						[ 'ready-classiceditorui', 'ready-datacontroller', 'ready-classiceditor' ] );
 
 					editor = newEditor;
 				} );
 		} );
 
-		it( 'fires uiReady once UI is rendered', () => {
+		it( 'fires ready once UI is rendered', () => {
 			let isReady;
 
 			class EventWatcher extends Plugin {
 				init() {
-					this.editor.on( 'uiReady', () => {
+					this.editor.ui.on( 'ready', () => {
 						isReady = this.editor.ui.view.isRendered;
 					} );
 				}
@@ -307,4 +311,17 @@ describe( 'ClassicEditor', () => {
 				} );
 		} );
 	} );
+
+	describeMemoryUsage( () => {
+		testMemoryUsage(
+			'should not grow on multiple create/destroy',
+			() => ClassicEditor
+				.create( document.querySelector( '#mem-editor' ), {
+					plugins: [ ArticlePluginSet ],
+					toolbar: [ 'heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote' ],
+					image: {
+						toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
+					}
+				} ) );
+	} );
 } );

+ 151 - 32
packages/ckeditor5-editor-classic/tests/classiceditorui.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /* globals document, Event */
@@ -10,20 +10,22 @@ import View from '@ckeditor/ckeditor5-ui/src/view';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import ClassicEditorUI from '../src/classiceditorui';
 import EditorUI from '@ckeditor/ckeditor5-core/src/editor/editorui';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import ClassicEditorUIView from '../src/classiceditoruiview';
 
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
-import utils from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+import { assertBinding } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+import { isElement } from 'lodash-es';
 
 describe( 'ClassicEditorUI', () => {
-	let editor, view, ui;
+	let editor, view, ui, viewElement;
 
 	testUtils.createSinonSandbox();
 
 	beforeEach( () => {
 		return VirtualClassicTestEditor
-			.create( {
+			.create( '', {
 				toolbar: [ 'foo', 'bar' ],
 			} )
 			.then( newEditor => {
@@ -31,6 +33,7 @@ describe( 'ClassicEditorUI', () => {
 
 				ui = editor.ui;
 				view = ui.view;
+				viewElement = view.element;
 			} );
 	} );
 
@@ -68,7 +71,7 @@ describe( 'ClassicEditorUI', () => {
 
 			it( 'sets view.stickyPanel#viewportTopOffset, when specified in the config', () => {
 				return VirtualClassicTestEditor
-					.create( {
+					.create( '', {
 						toolbar: {
 							viewportTopOffset: 100
 						}
@@ -89,41 +92,82 @@ describe( 'ClassicEditorUI', () => {
 				expect( ui.focusTracker.isFocused ).to.true;
 			} );
 
-			it( 'sets view.editable#name', () => {
-				const editable = editor.editing.view.document.getRoot();
-
-				expect( view.editable.name ).to.equal( editable.rootName );
-			} );
-
 			it( 'binds view.editable#isFocused', () => {
-				utils.assertBinding(
+				assertBinding(
 					view.editable,
 					{ isFocused: false },
 					[
-						[ editor.editing.view.document, { isFocused: true } ]
+						[ ui.focusTracker, { isFocused: true } ]
 					],
 					{ isFocused: true }
 				);
 			} );
 
-			it( 'binds view.editable#isReadOnly', () => {
+			it( 'set view.editable#name', () => {
 				const editable = editor.editing.view.document.getRoot();
 
-				utils.assertBinding(
-					view.editable,
-					{ isReadOnly: false },
-					[
-						[ editable, { isReadOnly: true } ]
-					],
-					{ isReadOnly: true }
-				);
+				expect( view.editable.name ).to.equal( editable.rootName );
+			} );
+		} );
+
+		describe( 'placeholder', () => {
+			it( 'sets placeholder from editor.config.placeholder', () => {
+				return VirtualClassicTestEditor
+					.create( 'foo', {
+						extraPlugins: [ Paragraph ],
+						placeholder: 'placeholder-text',
+					} )
+					.then( newEditor => {
+						const firstChild = newEditor.editing.view.document.getRoot().getChild( 0 );
+
+						expect( firstChild.getAttribute( 'data-placeholder' ) ).to.equal( 'placeholder-text' );
+
+						return newEditor.destroy();
+					} );
+			} );
+
+			it( 'sets placeholder from the "placeholder" attribute of a passed <textarea>', () => {
+				const element = document.createElement( 'textarea' );
+
+				element.setAttribute( 'placeholder', 'placeholder-text' );
+
+				return VirtualClassicTestEditor
+					.create( element, {
+						extraPlugins: [ Paragraph ]
+					} )
+					.then( newEditor => {
+						const firstChild = newEditor.editing.view.document.getRoot().getChild( 0 );
+
+						expect( firstChild.getAttribute( 'data-placeholder' ) ).to.equal( 'placeholder-text' );
+
+						return newEditor.destroy();
+					} );
+			} );
+
+			it( 'uses editor.config.placeholder rather than the "placeholder" attribute of a passed <textarea>', () => {
+				const element = document.createElement( 'textarea' );
+
+				element.setAttribute( 'placeholder', 'placeholder-text' );
+
+				return VirtualClassicTestEditor
+					.create( element, {
+						placeholder: 'config takes precedence',
+						extraPlugins: [ Paragraph ]
+					} )
+					.then( newEditor => {
+						const firstChild = newEditor.editing.view.document.getRoot().getChild( 0 );
+
+						expect( firstChild.getAttribute( 'data-placeholder' ) ).to.equal( 'config takes precedence' );
+
+						return newEditor.destroy();
+					} );
 			} );
 		} );
 
 		describe( 'view.toolbar#items', () => {
 			it( 'are filled with the config.toolbar (specified as an Array)', () => {
 				return VirtualClassicTestEditor
-					.create( {
+					.create( '', {
 						toolbar: [ 'foo', 'bar' ]
 					} )
 					.then( editor => {
@@ -138,7 +182,7 @@ describe( 'ClassicEditorUI', () => {
 
 			it( 'are filled with the config.toolbar (specified as an Object)', () => {
 				return VirtualClassicTestEditor
-					.create( {
+					.create( '', {
 						toolbar: {
 							items: [ 'foo', 'bar' ],
 							viewportTopOffset: 100
@@ -156,7 +200,7 @@ describe( 'ClassicEditorUI', () => {
 		} );
 
 		it( 'initializes keyboard navigation between view#toolbar and view#editable', () => {
-			return VirtualClassicTestEditor.create()
+			return VirtualClassicTestEditor.create( '' )
 				.then( editor => {
 					const ui = editor.ui;
 					const view = ui.view;
@@ -178,6 +222,73 @@ describe( 'ClassicEditorUI', () => {
 				} );
 		} );
 	} );
+
+	describe( 'destroy()', () => {
+		it( 'detaches the DOM root then destroys the UI view', () => {
+			return VirtualClassicTestEditor.create( '' )
+				.then( newEditor => {
+					const destroySpy = sinon.spy( newEditor.ui.view, 'destroy' );
+					const detachSpy = sinon.spy( newEditor.editing.view, 'detachDomRoot' );
+
+					return newEditor.destroy()
+						.then( () => {
+							sinon.assert.callOrder( detachSpy, destroySpy );
+						} );
+				} );
+		} );
+
+		it( 'restores the editor element back to its original state', () => {
+			const domElement = document.createElement( 'div' );
+
+			domElement.setAttribute( 'foo', 'bar' );
+			domElement.setAttribute( 'data-baz', 'qux' );
+			domElement.classList.add( 'foo-class' );
+
+			return VirtualClassicTestEditor.create( domElement )
+				.then( newEditor => {
+					return newEditor.destroy()
+						.then( () => {
+							const attributes = {};
+
+							for ( const attribute of Array.from( domElement.attributes ) ) {
+								attributes[ attribute.name ] = attribute.value;
+							}
+
+							expect( attributes ).to.deep.equal( {
+								foo: 'bar',
+								'data-baz': 'qux',
+								class: 'foo-class'
+							} );
+						} );
+				} );
+		} );
+	} );
+
+	describe( 'view()', () => {
+		it( 'returns view instance', () => {
+			expect( ui.view ).to.equal( view );
+		} );
+	} );
+
+	describe( 'element()', () => {
+		it( 'returns correct element instance', () => {
+			expect( ui.element ).to.equal( viewElement );
+		} );
+	} );
+
+	describe( 'getEditableElement()', () => {
+		it( 'returns editable element (default)', () => {
+			expect( ui.getEditableElement() ).to.equal( view.editable.element );
+		} );
+
+		it( 'returns editable element (root name passed)', () => {
+			expect( ui.getEditableElement( 'main' ) ).to.equal( view.editable.element );
+		} );
+
+		it( 'returns undefined if editable with the given name is absent', () => {
+			expect( ui.getEditableElement( 'absent' ) ).to.be.undefined;
+		} );
+	} );
 } );
 
 function viewCreator( name ) {
@@ -192,10 +303,14 @@ function viewCreator( name ) {
 }
 
 class VirtualClassicTestEditor extends VirtualTestEditor {
-	constructor( config ) {
+	constructor( sourceElementOrData, config ) {
 		super( config );
 
-		const view = new ClassicEditorUIView( this.locale );
+		if ( isElement( sourceElementOrData ) ) {
+			this.sourceElement = sourceElementOrData;
+		}
+
+		const view = new ClassicEditorUIView( this.locale, this.editing.view );
 		this.ui = new ClassicEditorUI( this, view );
 
 		this.ui.componentFactory.add( 'foo', viewCreator( 'foo' ) );
@@ -208,16 +323,20 @@ class VirtualClassicTestEditor extends VirtualTestEditor {
 		return super.destroy();
 	}
 
-	static create( config ) {
+	static create( sourceElementOrData, config ) {
 		return new Promise( resolve => {
-			const editor = new this( config );
+			const editor = new this( sourceElementOrData, config );
 
 			resolve(
 				editor.initPlugins()
 					.then( () => {
 						editor.ui.init();
-						editor.fire( 'uiReady' );
-						editor.fire( 'dataReady' );
+
+						const initialData = isElement( sourceElementOrData ) ?
+							sourceElementOrData.innerHTML :
+							sourceElementOrData;
+
+						editor.data.init( initialData );
 						editor.fire( 'ready' );
 					} )
 					.then( () => editor )

+ 13 - 19
packages/ckeditor5-editor-classic/tests/classiceditoruiview.js

@@ -1,22 +1,29 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-/* globals document */
-
 import ClassicEditorUIView from '../src/classiceditoruiview';
+import EditingView from '@ckeditor/ckeditor5-engine/src/view/view';
 import StickyPanelView from '@ckeditor/ckeditor5-ui/src/panel/sticky/stickypanelview';
 import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
 import InlineEditableUIView from '@ckeditor/ckeditor5-ui/src/editableui/inline/inlineeditableuiview';
 import Locale from '@ckeditor/ckeditor5-utils/src/locale';
+import createRoot from '@ckeditor/ckeditor5-engine/tests/view/_utils/createroot.js';
+
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
 describe( 'ClassicEditorUIView', () => {
-	let locale, view;
+	let locale, view, editingView, editingViewRoot;
+
+	testUtils.createSinonSandbox();
 
 	beforeEach( () => {
 		locale = new Locale( 'en' );
-		view = new ClassicEditorUIView( locale );
+		editingView = new EditingView();
+		editingViewRoot = createRoot( editingView.document );
+		view = new ClassicEditorUIView( locale, editingView );
+		view.editable.name = editingViewRoot.rootName;
 		view.render();
 	} );
 
@@ -67,17 +74,4 @@ describe( 'ClassicEditorUIView', () => {
 			} );
 		} );
 	} );
-
-	describe( 'editableElement', () => {
-		it( 'returns editable\'s view element', () => {
-			document.body.appendChild( view.element );
-
-			view.stickyPanel.limiterElement = view.element;
-
-			expect( view.editableElement.getAttribute( 'contentEditable' ) ).to.equal( 'true' );
-
-			view.element.remove();
-			view.destroy();
-		} );
-	} );
 } );

+ 4 - 4
packages/ckeditor5-editor-classic/tests/manual/classiceditor-data.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /* globals console:false, document, window */
@@ -28,7 +28,7 @@ function initEditor() {
 		.then( editor => {
 			counter += 1;
 			window.editors.push( editor );
-			container.appendChild( editor.element );
+			container.appendChild( editor.ui.element );
 		} )
 		.catch( err => {
 			console.error( err.stack );
@@ -39,7 +39,7 @@ function destroyEditors() {
 	window.editors.forEach( editor => {
 		editor.destroy()
 			.then( () => {
-				editor.element.remove();
+				editor.ui.element.remove();
 			} );
 	} );
 	window.editors = [];

+ 4 - 4
packages/ckeditor5-editor-classic/tests/manual/classiceditor.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /* globals console:false, document, window */
@@ -13,7 +13,7 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Undo from '@ckeditor/ckeditor5-undo/src/undo';
 import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
 import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
-import testUtils from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
+import { createObserver } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 let editor, editable, observer;
 
@@ -30,7 +30,7 @@ function initEditor() {
 			window.editor = editor = newEditor;
 			window.editable = editable = editor.editing.view.document.getRoot();
 
-			observer = testUtils.createObserver();
+			observer = createObserver();
 			observer.observe( 'Editable', editable, [ 'isFocused' ] );
 		} )
 		.catch( err => {

+ 20 - 0
packages/ckeditor5-editor-classic/tests/manual/memory.html

@@ -0,0 +1,20 @@
+<p>
+	<button id="destroyEditor">Destroy editor</button>
+	<button id="initEditor">Init editor</button>
+</p>
+
+<div id="editor">
+	<h2>Hello world!</h2>
+	<p>This is an editor instance.</p>
+
+	<img src="sample.jpg" />
+</div>
+
+<style>
+	.ck-editor {
+		margin-top: 200px;
+		margin-left: 100px;
+		margin-bottom: 200px;
+		width: 450px;
+	}
+</style>

+ 71 - 0
packages/ckeditor5-editor-classic/tests/manual/memory.js

@@ -0,0 +1,71 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals console, document */
+
+import ClassicEditor from '../../src/classiceditor';
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+
+/*
+ * Memory-leak safe version of classic editor manual test does not:
+ * - define global variables (such as let editor; in main file scope)
+ * - console.log() objects
+ * - add event listeners with () => {} methods which reference other
+ */
+function initEditor() {
+	let editor;
+
+	ClassicEditor
+		.create( document.querySelector( '#editor' ), {
+			plugins: [ ArticlePluginSet ],
+			toolbar: {
+				items: [
+					'heading',
+					'|',
+					'bold',
+					'italic',
+					'link',
+					'bulletedList',
+					'numberedList',
+					'blockQuote',
+					'insertTable',
+					'mediaEmbed',
+					'undo',
+					'redo'
+				]
+			},
+			image: {
+				toolbar: [
+					'imageStyle:full',
+					'imageStyle:side',
+					'|',
+					'imageTextAlternative'
+				]
+			},
+			table: {
+				contentToolbar: [
+					'tableColumn',
+					'tableRow',
+					'mergeTableCells'
+				]
+			}
+		} )
+		.then( newEditor => {
+			editor = newEditor;
+
+			document.getElementById( 'destroyEditor' ).addEventListener( 'click', destroyEditor );
+		} )
+		.catch( err => {
+			console.error( err.stack );
+		} );
+
+	function destroyEditor() {
+		editor.destroy().then( () => console.log( 'Editor was destroyed' ) );
+		editor = null;
+		document.getElementById( 'destroyEditor' ).removeEventListener( 'click', destroyEditor );
+	}
+}
+
+document.getElementById( 'initEditor' ).addEventListener( 'click', initEditor );

+ 31 - 0
packages/ckeditor5-editor-classic/tests/manual/memory.md

@@ -0,0 +1,31 @@
+1. Open DevTools on Chrome.
+2. Go to Memory tab.
+3. Select "Heap snapshot" profiling type.
+4. Click "Collect Garbage" (trash icon) and "Clear all profiles" (don't go icon).
+5. Record heap snapshot ("Take heap snapshot" a record icon).
+6. Repeat multiple times:
+    - click "Init editor",
+    - wait to editor show up ,
+    - click "Destroy editor".
+7. Record heap snapshot again.
+8. Compare Snapshot 1 with Snapshot 2 and look for:
+    - "detached" - HTML Elements/DOM nodes
+    - "EventListener" - there should be only 1
+    - Any CKEditor5 classes instances like "Editor" or "LivePosition"
+
+## Notes:
+
+* Browser extensions might attach event listeners to the DOM so the safest way to run this test is by running Chrome from command line:
+
+    ```
+    google-chrome \
+        --enable-precise-memory-info \
+        --disable-extensions \
+        --disable-plugins \
+        --incognito \
+        http://localhost:8125 
+    ```
+
+    The above will run Chrome without extensions or plugins in incognito mode and open manual tests page.
+
+* Close any running Chrome instance beforehand because otherwise it will open a tab in currently opened Chrome (look for "Opening in existing browser session." in command line).

+ 5 - 0
packages/ckeditor5-editor-classic/tests/manual/placeholder.html

@@ -0,0 +1,5 @@
+<textarea id="editor-1" placeholder="Placeholder from the attribute">
+	<p>Remove this text to see the placeholder.</p>
+</textarea>
+<br />
+<div id="editor-2" data-placeholder="Placeholder from the attribute"></div>

+ 37 - 0
packages/ckeditor5-editor-classic/tests/manual/placeholder.js

@@ -0,0 +1,37 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals console:false, document, window */
+
+import ClassicEditor from '../../src/classiceditor';
+import Enter from '@ckeditor/ckeditor5-enter/src/enter';
+import Typing from '@ckeditor/ckeditor5-typing/src/typing';
+import Heading from '@ckeditor/ckeditor5-heading/src/heading';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Undo from '@ckeditor/ckeditor5-undo/src/undo';
+import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
+import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
+
+window.editors = {};
+
+function initEditor( element, placeholder ) {
+	ClassicEditor
+		.create( element, {
+			plugins: [ Enter, Typing, Paragraph, Undo, Heading, Bold, Italic ],
+			toolbar: [ 'heading', '|', 'bold', 'italic', 'undo', 'redo' ],
+			placeholder
+		} )
+		.then( newEditor => {
+			console.log( 'Editor was initialized', newEditor );
+
+			window.editors[ element.id ] = newEditor;
+		} )
+		.catch( err => {
+			console.error( err.stack );
+		} );
+}
+
+initEditor( document.querySelector( '#editor-1' ) );
+initEditor( document.querySelector( '#editor-2' ), 'The placeholder from editor.config.placeholder' );

+ 8 - 0
packages/ckeditor5-editor-classic/tests/manual/placeholder.md

@@ -0,0 +1,8 @@
+## Editor placeholder
+
+1. Make sure the placeholder is visible in the editor without content.
+1. Focus the editor — the placeholder should disappear.
+1. Blur the editor — the placeholder should re–appear.
+1. Focus the editor with the content
+1. Remove the content, no placeholder should be visible.
+1. Blur the editor, the placeholder should appear.

BIN
packages/ckeditor5-editor-classic/tests/manual/sample.jpg


+ 2 - 2
packages/ckeditor5-editor-classic/tests/manual/tickets/60/1.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /* globals console:false, document, window */

+ 2 - 2
packages/ckeditor5-editor-classic/theme/classiceditor.css

@@ -1,6 +1,6 @@
 /*
- * Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 .ck.ck-editor {