diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3c3629e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+node_modules
diff --git a/README.md b/README.md
index 485191a..0ed435c 100644
--- a/README.md
+++ b/README.md
@@ -3,23 +3,21 @@ Emptyness — _How empty is **your** CKEditor 5 instance?_
Why does something so simple exist? With great simplicity comes great possibilities.
-...
-
-Well maybe _one_ possibility. The purpose of this plugin was to have an observable way to know if an editor instance has content or not.
+Well... maybe _one_ possibility. The purpose of this plugin was to have an observable way to know if an editor instance has content or not.
Specifically, how can an application know when an editor is empty so it can show a placeholder — like an input element.
## Documentation
-Simply build your editor with this plugin and you will get access to a `isEmpty` observable attribute (care of `ObservableMixin`) on your editor instance.
+Simply build your editor with this plugin and you will get access to a `isEmpty` observable attribute (care of `Observable`) on your editor instance. Additionaly, the plugin will add a `ck-editor__is-empty` class name on the editor element when is is empty.
-The origin use case was for the application to observe the attribute and know when the editor enters or exits an empty state. From there the application applies an attribute to the editors HTML element so that a CSS based placeholder can be applied — using CSS's `::before` rules to inject 'phantom' content without disturbing CKEditor's actual content, and no messy invisible HTML elements.
+The origin use case was for the application to observe the attribute and know when the editor enters or exits an empty state. From there the application applies an attribute to the editors HTML element so that a CSS based placeholder can be applied — using CSS's `::before` rules to inject _phantom_ content without disturbing CKEditor's actual content.
-### General Example
+### Placeholder Example
-```
+```js
import BalloonEditorBase from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor';
-import EmptynessPlugin from 'ckeditor5-emptyness/src/emptyness';
+import Emptyness from 'ckeditor5-emptyness/src/emptyness';
// .. all your other imports here
export default class MyEditor extends BalloonEditorBase { }
@@ -29,61 +27,62 @@ MyEditor.build = {
plugins: [
// ... all your other plugins here
- EmptynessPlugin
+ Emptyness
]
};
```
+```css
+.ck-editor__is-empty .ck-content.ck-editor__editable::before,
+.ck-editor__is-empty.ck-content.ck-editor__editable::before {
+ content: 'The editor is empty';
+ position: absolute;
+ display: block;
+
+ margin: var(--ck-spacing-large) 0;
+
+ color: #aaa;
+}
```
-const element = document.querySelector('#editor');
-MyEditor.create( element ).then( editor => {
+**_Whats with the weird CSS scope?_**
- editor.on( 'change:isEmpty', () => {
- console.log( 'is you empty or is you aint?', editor.isEmpty ? 'is empty' : 'aint empty' );
- } );
+Depending on the editor, the element used for content may not be the same as the one used when initialising the editor. Editors with toolbars will generate a new element structure. To cover all bases the scope is what it is -- where the `ck-editor__is-empty` class name is on the same element as the `ck-content` element or on a parent element.
-} );
-```
+## Changelog
-### Placeholder Example
-
-```
-const element = document.querySelector('#editor');
+### v0.2.0 - 4 July 2018
-MyEditor.create( element ).then( editor => {
+- Updating event listener to use `change:data` event on the editors document.
+- No more `data-empty`! The plugin now extends the editors template with a bound CSS class name.
+- Plugin now depends on CKEditor 5 core and engine `^10.1.0`.
+- We now have tests!
+- Updated README.
- editor.on( 'change:isEmpty', () => {
+### v0.1.0
- if ( editor.isEmpty ) {
- element.setAttribute( 'data-empty', true );
- } else {
- element.removeAttribute( 'data-empty' );
- }
+- Initial _rough_ version.
+- No tests.
- } );
-
-} );
-```
+## Testing
-```
-#editor[data-empty]::before {
- content: 'Untitled';
- position: absolute;
- display: block;
- color: #aaa;
- font-weight: bold;
-}
-```
+### Required setup
-**_Why can't we use CSS classes to use as a target for CSS?_**
+1. Setup the CKEditor [development environment](https://docs.ckeditor.com/ckeditor5/latest/framework/guides/contributing/development-environment.html).
+2. Add a dependency to the CKEditor project development environment:
+ 1. `cd ckeditor` enter into the CKEditor project as created in step 1
+ 2. Open and edit `mgit.json`
+ 3. Add `"ckeditor5-emptyness": "alexeckermann/ckeditor5-emptyness` to the dependencies list
+3. `mgit update`
+4. `lerna bootstrap --scope=ckeditor5-emptyness`
-The HTML element used for CKEditor instances is a managed view. Throughout use of the editor, the elements class list will change and can remove classes added outside of CKEditors management. In initial testing of this plugin we found no obvious way to extend the CKEditor's view `Template` for a editor view to enable CSS class based functionality. This could be resolved in future given advice from CKEditor maintainers.
+### Running tests
-## Testing
+1. `cd ckeditor` enter into the CKEditor project
+2. `npm run test -- --files=emptyness`
-TODO _Sorry..._
+It is important to note that tests are run from within the CKEditor project and not solely on this project.
## License
diff --git a/package.json b/package.json
index a29aba8..b9bda6d 100644
--- a/package.json
+++ b/package.json
@@ -1,15 +1,25 @@
{
"name": "ckeditor5-emptyness",
- "version": "0.1.0",
- "description": "How empty is your CKEditor instance? Adds isEmpty attribute to editors.",
+ "version": "0.2.0",
+ "description": "How empty is your CKEditor instance? Adds observable isEmpty property to editors.",
"keywords": [
"ckeditor5",
"ckeditor5-plugin"
],
"dependencies": {
- "@ckeditor/ckeditor5-core": "^10.0.0"
+ "@ckeditor/ckeditor5-core": "^10.1.0",
+ "@ckeditor/ckeditor5-engine": "^10.1.0"
+ },
+ "devDependencies": {
+ "@ckeditor/ckeditor5-editor-classic": "^10.0.1",
+ "@ckeditor/ckeditor5-essentials": "^10.1.0",
+ "@ckeditor/ckeditor5-paragraph": "^10.1.0",
+ "@ckeditor/ckeditor5-utils": "^10.1.0",
+ "eslint": "^4.15.0",
+ "eslint-config-ckeditor5": "^1.0.7",
+ "husky": "^0.14.3",
+ "lint-staged": "^7.0.0"
},
- "devDependencies": { },
"engines": {
"node": ">=6.0.0",
"npm": ">=3.0.0"
@@ -26,7 +36,5 @@
"files": [
"src"
],
- "scripts": {
-
- }
+ "scripts": { }
}
diff --git a/src/emptyness.js b/src/emptyness.js
index 3e16d18..f3c00b9 100644
--- a/src/emptyness.js
+++ b/src/emptyness.js
@@ -1,4 +1,5 @@
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import Template from '@ckeditor/ckeditor5-ui/src/template';
export default class Emptyness extends Plugin {
@@ -8,26 +9,29 @@ export default class Emptyness extends Plugin {
init() {
const editor = this.editor;
- const model = editor.data.model;
-
+ const doc = editor.model.document;
const view = editor.ui.view;
- const bind = view.bindTemplate;
- editor.set( 'isEmpty', !modelHasContent(model) );
+ editor.set( 'isEmpty', !documentHasContent(doc) );
- model.on( 'applyOperation', () => {
- editor.set( 'isEmpty', !modelHasContent(model) );
- return true;
+ this.listenTo( doc, 'change:data', () => {
+ editor.set( 'isEmpty', !documentHasContent(doc) );
+ } );
+
+ const bind = Template.bind( editor, editor );
+
+ view.extendTemplate( {
+ attributes: {
+ class: [
+ bind.if( 'isEmpty', 'ck-editor__is-empty' )
+ ]
+ }
} );
}
};
-function dataControllerRootHasContent(data) {
- return data.hasContent(data.model.getRoot());
-};
-
-function modelHasContent(model) {
- return model.hasContent(model.document.getRoot());
+function documentHasContent(doc) {
+ return doc.model.hasContent(doc.getRoot());
};
diff --git a/tests/emptyness.js b/tests/emptyness.js
new file mode 100644
index 0000000..9b787f6
--- /dev/null
+++ b/tests/emptyness.js
@@ -0,0 +1,110 @@
+import Emptyness from '../src/emptyness';
+
+import ModelText from '@ckeditor/ckeditor5-engine/src/model/text';
+import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+testUtils.createSinonSandbox();
+
+describe( 'Emptyness', () => {
+ let editor, editorElement;
+
+ const insertContent = () => {
+
+ editor.model.change( writer => {
+ writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
+ editor.model.insertContent( new ModelText( 'test' ), editor.model.document.selection );
+ } );
+
+ };
+
+ const removeAllContent = () => {
+
+ editor.model.change( writer => {
+ writer.setSelection( ModelRange.createIn( editor.model.document.getRoot().getChild( 0 ) ) );
+ editor.model.deleteContent( editor.model.document.selection );
+ } );
+
+ };
+
+ beforeEach( () => {
+ editorElement = document.createElement( 'div' );
+ document.body.appendChild( editorElement );
+
+ return ClassicTestEditor
+ .create( editorElement, {
+ plugins: [ Emptyness, Paragraph ]
+ } )
+ .then( newEditor => {
+ editor = newEditor;
+ } );
+ } );
+
+ afterEach( () => {
+ editorElement.remove();
+
+ return editor.destroy();
+ } );
+
+ it( 'should be loaded', () => {
+ expect( editor.plugins.get( Emptyness ) ).to.be.instanceOf( Emptyness );
+ } );
+
+ describe( 'init()', () => {
+
+ it( 'should set the isEmpty property', () => {
+ expect( editor.isEmpty ).to.be.true;
+ } );
+
+ } );
+
+ describe( 'isEmpty lifecycle', () => {
+
+ it( 'should be false when content is inserted', () => {
+ const setSpy = testUtils.sinon.spy( editor, 'set' );
+
+ insertContent();
+
+ sinon.assert.calledWithExactly( setSpy, 'isEmpty', false );
+ } );
+
+ it( 'should be true when all content is emptied', () => {
+ setData( editor.model, '