diff --git a/.babelrc b/.babelrc deleted file mode 100644 index facd1809..00000000 --- a/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015", "react"] -} \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..9c20d56f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,18 @@ +* text=auto + +*.htaccess eol=lf +*.cgi eol=lf +*.sh eol=lf + +*.css text +*.htm text +*.html text +*.js text +*.json text +*.php text +*.txt text +*.md text + +*.png -text +*.gif -text +*.jpg -text diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..027bd808 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ +### Suggested merge commit message ([convention](https://github.com/ckeditor/ckeditor5-design/wiki/Git-commit-message-convention)) + +Type: Message. Closes #000. + +--- + +### Additional information + +*For example – encountered issues, assumptions you had to make, other affected tickets, etc.* diff --git a/.gitignore b/.gitignore index 46f10721..25fbf5a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -lib/ -node_modules/ \ No newline at end of file +node_modules/ +coverage/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..580b310c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,25 @@ +sudo: required +dist: trusty +addons: + firefox: "latest" + apt: + sources: + - google-chrome + packages: + - google-chrome-stable +language: node_js +node_js: +- '8' +cache: +- node_modules +before_install: +- export DISPLAY=:99.0 +- sh -e /etc/init.d/xvfb start +install: +- npm install +script: +- npm run coverage && npm t -- --browsers=Firefox,BrowserStack_Edge,BrowserStack_Safari +env: + global: + - secure: ZNswdUStpHjer4LfNPhzIcsIUtv6R3wzaLIkQIhuXN+THZUwIBtPRjKU6XIBH29GoiSBsYrpjoq9m91iW9nkGhUFq+N/seeHvqDMj2eH5qmWK0jB8pPkrBBEe/70FdhncEeOhfX0KHhsFarpmmW6dcshrAvz35gp6DKuzs/a0QgV+XUnduc3OZ39yz5hQAaMfBJEaci0ZmMI52z9Q4Jf2FmljosznDQZ6ZU808vWxSZMeqknDqLcMRQ4X8okj1+7345K28X3uDetAY7EcmeBKUMJa/d/J1lzMD6DpqSwNiYRZ2FFXf8TZqZscCn4lB9SEW8YRkO1teO3stXQp+aeG7GS3o2c4nhNN4poeERmfa4c/ydSAGV+tsfzSV8Ye5pMUWfFtGvfiq8gkFNtLqL2B2N3fAhQlO5rYSx77f0mWpFGx3ys9UJCTUEExtpeK99ENSLQrgGukovxWRKZlbxDp4NxGnxK2RuSn0WmS1BUXRFxAxL0UdSkoeWDvw6me3WlfrQxEboR19BTHaMLFjJBrmbVylTHI7OsNrSFSuva/Mh2BKl3fYta2OnW2T9T79/NIsmgcDw9A9DeNwkFqdPaqCE+vgrEefly2oaUDl9nyCqu9yCkiATyVHkskniDhihSvz0FYScCYcyQ2OIUx2heMrXA1Nforhmnt7N2+3bnodE= + - secure: H++mTZEIu0MQr1zsxHhFcJ5Kuf8rspWOEnt9GyvJmE6YIaU4+cYmWcDorqOL/q9AftHGg65Rn4quoA4WWBpSkWoMctRl1ySp2r4qz+o9D6LM1Nm+HLXMkoEyB9eYVdXkaPO2BK9DgugLMwWSx1IT4l3lY6UIXFsJhdFpBB3mT9TfNC5mRcXtknM9I+7tcfgDKx1lkhl/YCBspyXmJdC/ntN3UlBofTGwPG92XWJ2JOZFEXm3H8gpSTy8m3KWPt1lOMlfT1tZWNEHWMVduYjMZz+AKqNY9OKe/nHDRoe5GRqTLvhnlSjGyrtBtMTj3f1lwJ6bez4lONiU8qNyDIZ2IHcCDjaP5im0hyY6zsczHrOLJeTajr4eQ7NxQERGgohu33wGVvNG7zLeRByZf85PQYTx28HcQRwYfCvEBA9sifs/VddiLyjkwFrrWx/dFbIwFL2/Fiwz6wQN/FLpg5bG9crCA2JMA/5+ua3HSXZtoZkF5W7bSS9+pHfgcU2DLEid9Q/IGxg/CKmMESITAXwyA8Mwd5YZVhargHLkUMRMMtjiXi30srbXsp/ICQXMuYr/0l/nnwQISZuBGnwSHfDuqhJxok5TdHnbhXvXNWkXcIJF7Iy7BEDe73XOE9VohszTXBSIqOMEkAdgMwNsxI8CXeIh68xZURPGatcd/g9K+Yg= diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..ea0b8688 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,4 @@ +Contributing +======================================== + +Information about contributing can be found on the following page: . diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..b7e29289 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,39 @@ +Software License Agreement +========================== + +@ckeditor/ckeditor5-react – Official CKEditor 5 React component. +Copyright (c) 2003-2018, CKSource – Frederico Knabben. All rights reserved. + +Licensed under the terms of the MIT License (see Appendix A): + + http://en.wikipedia.org/wiki/MIT_License + +Sources of Intellectual Property Included in CKEditor +----------------------------------------------------- + +Where not otherwise indicated, all @ckeditor/jsdoc-plugins content is authored by CKSource engineers and consists of CKSource-owned intellectual property. In some specific instances, @ckeditor/jsdoc-plugins will incorporate work done by developers outside of CKSource with their express permission. + +Appendix A: The MIT License +--------------------------- + +The MIT License (MIT) + +Copyright (c) 2003-2018, CKSource – Frederico Knabben + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..abedeed9 --- /dev/null +++ b/README.md @@ -0,0 +1,396 @@ +# CKEditor 5 Component for React + +[![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-react.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-react) +[![Build Status](https://travis-ci.org/ckeditor/ckeditor5-react.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-react) +[![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5-react/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5-react?branch=master) +
+[![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-react/status.svg)](https://david-dm.org/ckeditor/ckeditor5-react) +[![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-react/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-react?type=dev) + +Official CKEditor 5 React component. + +## Using with ready to use CKEditor 5 builds + +There are pre-build versions of CKEditor 5 that you can choose from: + +* [CKEditor 5 classic editor build](https://www.npmjs.com/package/@ckeditor/ckeditor5-build-classic) +* [CKEditor 5 inline editor build](https://www.npmjs.com/package/@ckeditor/ckeditor5-build-inline) +* [CKEditor 5 balloon editor build](https://www.npmjs.com/package/@ckeditor/ckeditor5-build-balloon) +* [CKEditor 5 decoupled editor build](https://www.npmjs.com/package/@ckeditor/ckeditor5-build-decoupled-document) + +Install bindings and one of the builds: + +```bash +npm install --save @ckeditor/ckeditor5-react @ckeditor/ckeditor5-build-classic +``` + +Use CKEditor component inside your project: + +```jsx +import React, { Component } from 'react'; +import CKEditor from '@ckeditor/ckeditor5-react'; +import ClassicEditorBuild from '@ckeditor/ckeditor5-build-classic'; + +class App extends Component { + render() { + return ( +
+

CKEditor 5 using build-classic

+ console.log( { event, editor } ) } + /> +
+ ); + } +} + +export default App; +``` + +## Available properties + +* `editor` _required_ - a class that represents the [Editor](https://docs.ckeditor.com/ckeditor5/latest/api/module_core_editor_editor-Editor.html), +* `data` - an initial data for the created editor, see the [`DataApi#setData()`](https://docs.ckeditor.com/ckeditor5/latest/api/module_core_editor_utils_dataapimixin-DataApi.html#function-setData) method, +* `config` - an object that implements the [`EditorConfig`](https://docs.ckeditor.com/ckeditor5/latest/api/module_core_editor_editorconfig-EditorConfig.html) interface, +* `onChange` - a function that will be called when the editor's document was changed, see the [`model.Document#change`](https://docs.ckeditor.com/ckeditor5/latest/api/module_engine_model_document-Document.html#event-change) event, + It receives two parameters: + 1. an [`EventInfo`](https://docs.ckeditor.com/ckeditor5/latest/api/module_utils_eventinfo-EventInfo.html) object, + 2. an [`Editor`](https://docs.ckeditor.com/ckeditor5/latest/api/module_core_editor_editor-Editor.html) instance. +* `onInit` - a function that is calling once immediately when the editor was initialized. It receives the initialized [`editor`](https://docs.ckeditor.com/ckeditor5/latest/api/module_core_editor_editor-Editor.html) as a parameter. + +## Building custom editor together with your React project + +This guide is assuming that you are using [Create React App CLI](https://github.com/facebook/create-react-app) as your boilerplate. +If not please read more about webpack configuration [here](https://docs.ckeditor.com/ckeditor5/latest/framework/guides/quick-start.html#lets-start). + +Install React CLI: + +```bash +npm install -g create-react-app +``` + +Create your project using the CLI and go to the project's directory: + +```bash +create-react-app ckeditor5-react-example && cd ckeditor5-react-example +``` + +Ejecting configuration is needed for custom webpack configuration to load inline SVG images. +More information about ejecting can be found [here](https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#npm-run-eject). + +```bash +npm run eject +``` + +### `npm run build` failed to minify the code + +```bash +Failed to minify the code from this file: [31/75] + /node_modules/@ckeditor/ckeditor5-build-classic/build/ckeditor.js:5:2077 +``` + +The UglifyJS exported by Webpack cannot parse a code written ES6. You need to manually replace it with `uglifyjs-webpack-plugin`. These changes touches `webpack.config.prod.js` file only. + +After ejecting this file is placed in `/config/webpack.config.prod.js`. + +1. Install `uglifyjs-webpack-plugin`. + +```bash +npm install --save-dev uglifyjs-webpack-plugin +``` + +2. Load installed package (at the top of `webpack.config.prod.js` file). + +```js +const UglifyJsWebpackPlugin = require( 'uglifyjs-webpack-plugin' ); +``` + +3. Replace the `webpack.optimize.UglifyJsPlugin` with `UglifyJsWebpackPlugin` + +```diff +- new webpack.optimize.UglifyJsPlugin ++ new UglifyJsWebpackPlugin +``` + +Options: `compress`, `mangle` and `output` are invaild for `UglifyJsWebpackPlugin`. You need to wrap these option as `uglifyOptions`. +The whole plugin definition should look like: + +```js +// Minify the code. +new UglifyJsWebpackPlugin( { + uglifyOptions: { + compress: { + warnings: false, + // Disabled because of an issue with Uglify breaking seemingly valid code: + // https://github.com/facebookincubator/create-react-app/issues/2376 + // Pending further investigation: + // https://github.com/mishoo/UglifyJS2/issues/2011 + comparisons: false, + }, + mangle: { + safari10: true, + }, + output: { + comments: false, + // Turned on because emoji and regex is not minified properly using default + // https://github.com/facebookincubator/create-react-app/issues/2488 + ascii_only: true, + }, + }, + sourceMap: shouldUseSourceMap, +}) +``` + +### Changes required for both Webpack configs (`webpack.config.dev.js` and `webpack.config.prod.js`) + +In order to build your application properly, you need to modify your webpack configuration files. After ejecting they are located at: + +```bash +/config/webpack.config.dev.js +/config/webpack.config.prod.js +``` + +We need to modify webpack configuration to load CKEditor 5 SVG icons properly. + +At the beginning import an object that creates a configuration for PostCSS: + +```js +const { styles } = require( '@ckeditor/ckeditor5-dev-utils' ); +``` + +Then add two new elements to exported object under `module.rules` array (as new items for `oneOf` array). These are SVG and CSS loaders only for CKEditor 5 code: + +```js +{ + test: /ckeditor5-[^/\\]+[/\\]theme[/\\]icons[/\\][^/\\]+\.svg$/, + use: [ 'raw-loader' ] +}, +{ + test: /ckeditor5-[^/\\]+[/\\]theme[/\\].+\.css/, + use: [ + { + loader: 'style-loader', + options: { + singleton: true + } + }, + { + loader: 'postcss-loader', + options: styles.getPostCssConfig( { + themeImporter: { + themePath: require.resolve( '@ckeditor/ckeditor5-theme-lark' ) + }, + minify: true + } ) + } + ] +}, +``` + +Exclude CSS files used by CKEditor 5 from project's CSS loader: + +```js +{ + test: /\.css$/, + exclude: /ckeditor5-[^/\\]+[/\\]theme[/\\].+\.css/, + // (...) +} +``` + +and exclude CKEditor 5 SVG and CSS files from `file-loader` because these files will be handled by the loaders added previously +(usually the last item in `module.rules` array is the `file-loader`) so it looks like this: + +```js +{ + loader: require.resolve('file-loader'), + // Exclude `js` files to keep "css" loader working as it injects + // it's runtime that would otherwise processed through "file" loader. + // Also exclude `html` and `json` extensions so they get processed + // by webpacks internal loaders. + exclude: [ + /\.(js|jsx|mjs)$/, + /\.html$/, + /\.json$/, + /ckeditor5-[^/\\]+[/\\]theme[/\\]icons[/\\][^/\\]+\.svg$/, + /ckeditor5-[^/\\]+[/\\]theme[/\\].+\.css/ + ], + options: { + name: 'static/media/[name].[hash:8].[ext]' + } +} +``` + +**Remember that the changes above should be done in both configuration files.** + +Next, install `raw-loader`, theme for CKEditor 5 and CKEditor 5 dev-utils: + +```bash +npm install raw-loader @ckeditor/ckeditor5-theme-lark @ckeditor/ckeditor5-dev-utils --save-dev +``` + +Install bindings, editor and plugins you need: + +```bash +npm install --save \ + @ckeditor/ckeditor5-react \ + @ckeditor/ckeditor5-editor-classic \ + @ckeditor/ckeditor5-essentials \ + @ckeditor/ckeditor5-basic-styles \ + @ckeditor/ckeditor5-heading \ + @ckeditor/ckeditor5-paragraph +``` + +### Use CKEditor component together with [CKEditor 5 framework](https://docs.ckeditor.com/ckeditor5/latest/framework/): + +```jsx +import React, { Component } from 'react'; +import CKEditor from '@ckeditor/ckeditor5-react'; + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; +import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; +import Heading from '@ckeditor/ckeditor5-heading/src/heading'; + +class App extends Component { + render() { + return ( +
+

CKEditor 5 using custom build

+ console.log( 'Editor is ready to use!', editor ) } + onChange={ ( event, editor ) => console.log( { event, editor } ) } + config={ { + plugins: [ Essentials, Paragraph, Bold, Italic, Heading ], + toolbar: [ 'heading', '|', 'bold', 'italic', '|', 'undo', 'redo', ] + } } + editor={ ClassicEditor } + data="

Hello from CKEditor 5!

" + /> +
+ ); + } +} + +export default App; +``` + +### Use CKEditor component together with [CKEditor5 builds](https://docs.ckeditor.com/ckeditor5/latest/builds/) + +```jsx +import React, { Component } from 'react'; +import CKEditor from '@ckeditor/ckeditor5-react'; + +import ClassicEditor from '@ckeditor/ckeditor5-build-classic'; + +class App extends Component { + render() { + return ( +
+

CKEditor 5 using custom build

+ console.log( 'Editor is ready to use!', editor ) } + onChange={ ( event, editor ) => console.log( { event, editor } ) } + editor={ ClassicEditor } + data="

Hello from CKEditor 5!

" + /> +
+ ); + } +} + +export default App; +``` + +#### Document editor + +If you use the [`Document editor`](https://docs.ckeditor.com/ckeditor5/latest/framework/guides/ui/document-editor.html), [you need to add the toolbar manually](https://docs.ckeditor.com/ckeditor5/latest/api/module_editor-decoupled_decouplededitor-DecoupledEditor.html#static-function-create): + +```jsx +import DecoupledEditor from '@ckeditor/ckeditor5-editor-decoupled/src/decouplededitor'; +// Import plugins that you need... + +class App extends Component { + render() { + return ( +
+

CKEditor 5 using custom build - DecoupledEditor

+ { + console.log( 'Editor is ready to use!', editor ); + + // Inserts the toolbar before the editable area. + editor.ui.view.editable.element.parentElement.insertBefore( + editor.ui.view.toolbar.element, + editor.ui.view.editable.element + ); + } } + onChange={ ( event, editor ) => console.log( { event, editor } ) } + editor={ DecoupledEditor } + data="

Hello from CKEditor 5 as DecoupledEditor!

" + config={ /* the editor configuration */ } + /> + ); + } +} + +export default App; +``` + +## Development the repository + +#### Executing tests + +```bash +npm run tests -- [additional options] +# or +npm t -- [additional options] +``` + +It accepts few options: + +* `coverage` (`-c`) - whether generates the code coverage, +* `source-map` (`-s`) - whether attaches the source maps, +* `watch` (`-w`) - whether watch testes files, +* `reporter` (`-r`) - reporter for the Karma (default: `mocha`, can be changed to `dots`) +* `browsers` (`-b`) - browsers that will be used to run tests (default: `Chrome`, available: `Firefox`, `BrowserStack_Edge` and `BrowserStack_Safari`) + +**Note:** + +If you would like to use the `BrowserStack_*` browser, you need to specify the `BROWSER_STACK_USERNAME` and `BROWSER_STACK_ACCESS_KEY` as +an environment variable, e.g.: + +```bass +BROWSER_STACK_USERNAME=[...] BROWSER_STACK_ACCESS_KEY=[...] npm t -- -b BrowserStack_Edge,BrowserStack_Safari -c +``` + +If you are going to change the source (`src/ckeditor.jsx`) file, remember about rebuilding the package. You can use `npm run develop` +in order to do it automatically. + +#### Generate changelog + +```bash +npm run changelog +``` + +#### Creating release + +Before starting releasing the package, you need to generate a changelog. + +```bash +npm run release +``` + +Note: Only the `dist/` directory will be published. + +#### Build the package + +It builds a minified version of the package which is ready to publish. + +```bash +npm run build +``` diff --git a/dist/ckeditor.js b/dist/ckeditor.js new file mode 100644 index 00000000..420fbcc2 --- /dev/null +++ b/dist/ckeditor.js @@ -0,0 +1,6 @@ +/*! + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("react")):"function"==typeof define&&define.amd?define(["react"],t):"object"==typeof exports?exports.CKEditor=t(require("react")):e.CKEditor=t(e.React)}(window,function(e){return function(e){var t={};function n(o){if(t[o])return t[o].exports;var r=t[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,n),r.l=!0,r.exports}return n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(o,r,function(t){return e[t]}.bind(null,r));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=4)}([function(e,t,n){"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},function(e,t,n){"use strict";var o=n(0);function r(){}e.exports=function(){function e(e,t,n,r,i,u){if(u!==o){var a=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw a.name="Invariant Violation",a}}function t(){return e}e.isRequired=e;var n={array:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t};return n.checkPropTypes=r,n.PropTypes=n,n}},function(e,t,n){e.exports=n(1)()},function(t,n){t.exports=e},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=function(){function e(e,t){for(var n=0;n element which will be replaced by CKEditor.\n\trender() {\n\t\treturn (\n\t\t\t
( this.domContainer = ref ) }>
\n\t\t);\n\t}\n\n\t_initializeEditor() {\n\t\tthis.props.editor\n\t\t\t.create( this.domContainer, this.props.config )\n\t\t\t.then( editor => {\n\t\t\t\tthis.editor = editor;\n\n\t\t\t\tif ( this.props.data ) {\n\t\t\t\t\tthis.editor.setData( this.props.data );\n\t\t\t\t}\n\n\t\t\t\tif ( this.props.onInit ) {\n\t\t\t\t\tthis.props.onInit( this.editor );\n\t\t\t\t}\n\n\t\t\t\tconst document = this.editor.model.document;\n\n\t\t\t\tdocument.on( 'change:data', event => {\n\t\t\t\t\t/* istanbul ignore else */\n\t\t\t\t\tif ( this.props.onChange ) {\n\t\t\t\t\t\tthis.props.onChange( event, editor );\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t} )\n\t\t\t.catch( error => {\n\t\t\t\tconsole.error( error );\n\t\t\t} );\n\t}\n\n\t_destroyEditor() {\n\t\tif ( this.editor ) {\n\t\t\tthis.editor.destroy()\n\t\t\t\t.then( () => {\n\t\t\t\t\tthis.editor = null;\n\t\t\t\t} );\n\t\t}\n\t}\n}\n\n// Properties definition.\nCKEditor.propTypes = {\n\teditor: PropTypes.func.isRequired,\n\tdata: PropTypes.string,\n\tconfig: PropTypes.object,\n\tonChange: PropTypes.func,\n\tonInit: PropTypes.func\n};\n\n// Default values for non-required properties.\nCKEditor.defaultProps = {\n\tdata: '',\n\tconfig: {}\n};\n\n"],"sourceRoot":""} \ No newline at end of file diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 00000000..5d766f76 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,266 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +/* eslint-env node */ + +const path = require( 'path' ); + +const options = parseArguments( process.argv.slice( 2 ) ); + +module.exports = function getKarmaConfig( config ) { + const basePath = process.cwd(); + const coverageDir = path.join( basePath, 'coverage' ); + + const webpackConfig = { + mode: 'development', + + resolve: { + alias: { + React: 'react', + } + }, + + module: { + rules: [ + { + test: /\.jsx?$/, + exclude: /@ckeditor\/ckeditor5-build/, + loader: 'babel-loader', + query: { + compact: false, + presets: [ 'react', 'env' ] + } + } + ] + } + }; + + const karmaConfig = { + basePath, + + frameworks: [ 'mocha', 'chai', 'sinon' ], + + files: [ + 'tests/**/*.jsx' + ], + + preprocessors: { + 'tests/**/*.jsx': [ 'webpack' ] + }, + + webpack: webpackConfig, + + webpackMiddleware: { + noInfo: true, + stats: 'minimal' + }, + + reporters: [ options.reporter ], + + port: 9876, + + colors: true, + + logLevel: 'INFO', + + browsers: getBrowsers( options.browsers ), + + customLaunchers: { + CHROME_TRAVIS_CI: { + base: 'Chrome', + flags: [ '--no-sandbox', '--disable-background-timer-throttling' ] + }, + CHROME_LOCAL: { + base: 'Chrome', + flags: [ '--disable-background-timer-throttling' ] + }, + BrowserStack_Edge: { + base: 'BrowserStack', + os: 'Windows', + os_version: '10', + browser: 'edge' + }, + BrowserStack_Safari: { + base: 'BrowserStack', + os: 'OS X', + os_version: 'High Sierra', + browser: 'safari' + } + }, + + singleRun: true, + + concurrency: Infinity, + + browserNoActivityTimeout: 0, + + mochaReporter: { + showDiff: true + } + }; + + if ( shouldEnableBrowserStack() ) { + karmaConfig.browserStack = { + username: process.env.BROWSER_STACK_USERNAME, + accessKey: process.env.BROWSER_STACK_ACCESS_KEY, + build: process.env.TRAVIS_REPO_SLUG, + project: 'ckeditor5' + }; + + karmaConfig.reporters = [ 'dots', 'BrowserStack' ]; + } + + if ( options.watch ) { + karmaConfig.autoWatch = true; + karmaConfig.singleRun = false; + } + + if ( options.coverage ) { + karmaConfig.reporters.push( 'coverage' ); + + if ( process.env.TRAVIS ) { + karmaConfig.reporters.push( 'coveralls' ); + } + + karmaConfig.coverageReporter = { + reporters: [ + // Prints a table after tests result. + { + type: 'text-summary' + }, + // Generates HTML tables with the results. + { + dir: coverageDir, + type: 'html' + }, + // Generates "lcov.info" file. It's used by external code coverage services. + { + type: 'lcovonly', + dir: coverageDir + } + ] + }; + + webpackConfig.module.rules.push( { + test: /\.jsx?$/, + loader: 'istanbul-instrumenter-loader', + include: /src/, + exclude: [ + /node_modules/ + ], + query: { + esModules: true + } + } ); + } + + if ( options.sourceMap ) { + karmaConfig.preprocessors[ 'tests/**/*.jsx' ].push( 'sourcemap' ); + + webpackConfig.devtool = 'inline-source-map'; + } + + config.set( karmaConfig ); +}; + +/** + * Returns the value of Karma's browser option. + * + * @param {Array.} browsers + * @returns {Array.|null} + */ +function getBrowsers( browsers ) { + if ( !browsers ) { + return null; + } + + const newBrowsers = browsers + .map( browser => { + if ( browser !== 'Chrome' ) { + return browser; + } + + return process.env.TRAVIS ? 'CHROME_TRAVIS_CI' : 'CHROME_LOCAL'; + } ); + + if ( shouldEnableBrowserStack() ) { + return newBrowsers; + } + + // If the BrowserStack is disabled, all browsers that start with a prefix "BrowserStack" should be filtered out. + // See: https://github.com/ckeditor/ckeditor5-dev/issues/358 and https://github.com/ckeditor/ckeditor5-dev/issues/402. + return newBrowsers.filter( browser => !browser.startsWith( 'BrowserStack' ) ); +} + +function shouldEnableBrowserStack() { + if ( !process.env.BROWSER_STACK_USERNAME ) { + return false; + } + + if ( !process.env.BROWSER_STACK_ACCESS_KEY ) { + return false; + } + + // If the repository slugs are different, the pull request comes from the community (forked repository). + // For such builds, BrowserStack will be disabled. Read more: https://github.com/ckeditor/ckeditor5-dev/issues/358. + return ( process.env.TRAVIS_EVENT_TYPE !== 'pull_request' || process.env.TRAVIS_PULL_REQUEST_SLUG === process.env.TRAVIS_REPO_SLUG ); +} + +/** + * @param {Array.} args CLI arguments and options. + * @returns {Object} options + * @returns {Array.} options.browsers Browsers that will be used to run tests. + * @returns {String} options.reporter A reporter that will presents tests results. + * @returns {Boolean} options.watch Whether to watch the files. + * @returns {Boolean} options.coverage Whether to generate code coverage. + * @returns {Boolean} options.sourceMap Whether to add source maps. + */ +function parseArguments( args ) { + const minimist = require( 'minimist' ); + + const config = { + string: [ + 'browsers', + 'reporter' + ], + + boolean: [ + 'watch', + 'coverage', + 'source-map' + ], + + alias: { + b: 'browsers', + c: 'coverage', + r: 'reporter', + s: 'source-map', + w: 'watch', + }, + + default: { + browsers: 'Chrome', + reporter: 'mocha', + watch: false, + coverage: false, + 'source-map': false, + } + }; + + const options = minimist( args, config ); + + options.sourceMap = options[ 'source-map' ]; + options.browsers = options.browsers.split( ',' ); + + // Delete all aliases because we don't want to use them in the code. + // They are useful when calling command but useless after that. + for ( const alias of Object.keys( config.alias ) ) { + delete options[ alias ]; + } + + return options; +} diff --git a/package.json b/package.json index ae5432ef..a2ae2a08 100644 --- a/package.json +++ b/package.json @@ -1,30 +1,73 @@ { - "name": "cke5-react", + "name": "@ckeditor/ckeditor5-react", "version": "0.1.0", - "description": "CKEditor 5 bindings for react.", - "main": "lib/ckeditor.js", + "description": "Official CKEditor 5 React component.", + "main": "dist/ckeditor.js", + "keywords": [ + "ckeditor", + "ckeditor5", + "ckeditor 5", + "ckeditor5-feature", + "react" + ], "dependencies": { - "prop-types": "^15.6.1", - "react": "^16.3.2" + "prop-types": "^15.6.1" }, "devDependencies": { - "babel-cli": "^6.26.0", - "babel-preset-es2015": "^6.24.1", - "babel-preset-react": "^6.24.1" + "@ckeditor/ckeditor5-build-classic": "^11.0.1", + "@ckeditor/ckeditor5-dev-env": "^11.1.1", + "@ckeditor/ckeditor5-dev-utils": "^10.0.3", + "babel-core": "^6.26.3", + "babel-loader": "^7.1.5", + "babel-preset-env": "^1.7.0", + "babel-preset-react": "^6.24.1", + "chai": "^4.1.2", + "enzyme": "^3.3.0", + "enzyme-adapter-react-16": "^1.1.1", + "istanbul-instrumenter-loader": "^3.0.0", + "karma": "^2.0.0", + "karma-browserstack-launcher": "^1.3.0", + "karma-chai": "^0.1.0", + "karma-chrome-launcher": "^2.2.0", + "karma-coverage": "^1.1.1", + "karma-coveralls": "^2.0.0", + "karma-firefox-launcher": "^1.0.1", + "karma-mocha": "^1.3.0", + "karma-mocha-reporter": "^2.2.4", + "karma-sinon": "^1.0.5", + "karma-sourcemap-loader": "^0.3.7", + "karma-webpack": "^3.0.0", + "minimist": "^1.2.0", + "mocha": "^5.2.0", + "react": "^16.0.0", + "react-dom": "^16.0.0", + "sinon": "^6.1.3", + "uglifyjs-webpack-plugin": "^1.2.7", + "webpack": "^4.16.0" + }, + "peerDependencies": { + "react": "^16.0.0" }, "scripts": { - "prepublish": "babel --out-dir ./lib ./src", - "develop": "babel --out-dir ./lib ./src --watch", - "test": "echo \"Error: no test specified\" && exit 1" + "build": "webpack --mode production", + "develop": "webpack --mode development --watch", + "preversion": "npm run build; if [ -n \"$(git status dist/ --porcelain)\" ]; then git add -u dist/ && git commit -m 'Internal: Build.'; fi", + "changelog": "node ./scripts/changelog.js", + "release": "node ./scripts/release.js", + "test": "karma start", + "coverage": "karma start --coverage" }, "repository": { "type": "git", - "url": "git+https://github.com/szymonkups/cke5-react.git" + "url": "https://github.com/ckeditor/ckeditor5-react.git" }, - "author": "", - "license": "ISC", + "author": "CKSource (http://cksource.com/)", + "license": "MIT", "bugs": { - "url": "https://github.com/szymonkups/cke5-react/issues" + "url": "https://github.com/ckeditor/ckeditor5-react/issues" }, - "homepage": "https://github.com/szymonkups/cke5-react#readme" + "homepage": "https://github.com/ckeditor/ckeditor5-react", + "files": [ + "dist" + ] } diff --git a/readme.md b/readme.md deleted file mode 100644 index 148e26ec..00000000 --- a/readme.md +++ /dev/null @@ -1,208 +0,0 @@ -# CKEditor 5 bindings for React - -

⚠⚠ ⚠⚠ ⚠⚠ - -

NOTE: This package is not yet published on npm. It's a developer's preview available only through GitHub. - -

See https://github.com/ckeditor/ckeditor5-react/issues/5 for more information. - -## Using with ready to use CKEditor 5 builds - -There are pre-build versions of CKEditor 5 that you can choose from: -* [CKEditor 5 classic editor build](https://www.npmjs.com/package/@ckeditor/ckeditor5-build-classic) -* [CKEditor 5 inline editor build](https://www.npmjs.com/package/@ckeditor/ckeditor5-build-inline) -* [CKEditor 5 balloon editor build](https://www.npmjs.com/package/@ckeditor/ckeditor5-build-balloon) - -Install bindings and one of the builds: - -``` -npm install --save cke5-react @ckeditor/ckeditor5-build-classic -``` - -Use CKEditor component inside your project: -```js -import React, { Component } from 'react'; -import './App.css'; -import CKEditor from 'cke5-react'; -import ClassicEditorBuild from '@ckeditor/ckeditor5-build-classic'; - - -class App extends Component { - render() { - return ( -

-

CKEditor 5 using build-classic

- console.log( data ) } - /> -
- ); - } -} - -export default App; -``` - -##### TODO: Even after adding CKEditor 5 build to the babel process it producess some errors in Create React App production: -```js -41:13-31 "export 'default' (imported as 'ClassicEditorBuild') was not found in '@ckeditor/ckeditor5-build-classic/build/ckeditor' -``` - -## Building custom editor together with your React project - -This guide is assuming that you are using [Create React App CLI](https://github.com/facebook/create-react-app) as your -boilerplate. If not please read more about webpack configuration [here](https://docs.ckeditor.com/ckeditor5/latest/framework/guides/quick-start.html#lets-start). - -Install React CLI: -``` -npm install -g create-react-app -``` - -Create your project using the CLI and go to the project's directory: -``` -create-react-app ckeditor5-react-example && cd ckeditor5-react-example -``` - -Ejecting configuration is needed for custom webpack configuration to load inline SVG images. -More information about ejecting can be found [here](https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#npm-run-eject). - -``` -npm run eject -``` - -We need to modify webpack configuration scripts to load CKEditor 5 SVG icons properly. After ejecting they are located at -``` -/config/webpack.config.dev.js -/config/webpack.config.prod.js -``` - -### Changes that need to be made to both config files (webpack.config.dev.js and webpack.config.prod.js) - -In both files add two new elements to exported object under `module.rules` array, these are SVG and CSS loaders only for CKEditor 5 code: -```js -{ - test: /ckeditor5-[^/]+\/theme\/icons\/[^/]+\.svg$/, - use: [ 'raw-loader' ] -}, -{ - test: /ckeditor5-[^/]+\/theme\/.+\.css/, - use: [ - { - loader: 'style-loader', - options: { - singleton: true - } - }, - { - loader: 'postcss-loader', - options: styles.getPostCssConfig( { - themeImporter: { - themePath: require.resolve( '@ckeditor/ckeditor5-theme-lark' ) - }, - minify: true - } ) - } - ] -}, -``` - -Exclude CSS files used by CKEditor 5 from project's CSS loader: - -```js -{ - test: /\.css$/, - exclude: /ckeditor5-[^/]+\/theme\/.+\.css/, - (...) -``` - -and exclude CKEditor 5 SVG and CSS files from `file-loader` (usually the last item in `module.rules` array) so it looks like this: - -```js -{ - loader: require.resolve('file-loader'), - // Exclude `js` files to keep "css" loader working as it injects - // it's runtime that would otherwise processed through "file" loader. - // Also exclude `html` and `json` extensions so they get processed - // by webpacks internal loaders. - exclude: [ - /\.(js|jsx|mjs)$/, - /\.html$/, - /\.json$/, - /ckeditor5-[^/]+\/theme\/icons\/[^/]+\.svg$/, - /ckeditor5-[^/]+\/theme\/.+\.css/ - ], - options: { - name: 'static/media/[name].[hash:8].[ext]' - } -} -``` - -Next, install `raw-loader`: -``` -npm install --save-dev raw-loader -``` - -Install bindings, editor and plugins you need: - -``` -npm install --save \ - cke5-react \ - @ckeditor/ckeditor5-editor-classic \ - @ckeditor/ckeditor5-essentials/src/essentials \ - @ckeditor/ckeditor5-basic-styles \ - @ckeditor/ckeditor5-heading -``` - -### Changes in webpack.config.prod.js only -CKEditor 5 files are not transpiled to ES5 by default. Add CKEditor 5 files to be processed by Babel: - -```js -// Process JS with Babel. -{ - test: /\.(js|jsx|mjs)$/, - include: [ - paths.appSrc, - path.resolve( 'node_modules', '@ckeditor' ) - ], - (...) -``` - - -### Use CKEditor component inside your project: - -```js -import React, { Component } from 'react'; -import './App.css'; -import CKEditor from 'cke5-react'; - -import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; -import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials'; -import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; -import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; -import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; -import Heading from '@ckeditor/ckeditor5-heading/src/heading'; - - -class App extends Component { - render() { - return ( -
-

CKEditor 5 using custom build

- console.log( data ) } - config={ { - plugins: [ Essentials, Paragraph, Bold, Italic, Heading ], - toolbar: [ 'heading', '|', 'bold', 'italic', '|', 'undo', 'redo', ] - } } - editor={ ClassicEditor } - data="

Hello from CKEditor 5

" - /> -
- ); - } -} - -export default App; -``` diff --git a/sample/index.html b/sample/index.html new file mode 100644 index 00000000..dd2c399e --- /dev/null +++ b/sample/index.html @@ -0,0 +1,49 @@ + + + + + CKEditor 5 – React Component – development sample + + + +

CKEditor 5 – React Component – development sample

+
+ + + + + + + diff --git a/sample/sample.jpg b/sample/sample.jpg new file mode 100644 index 00000000..b77d07e7 Binary files /dev/null and b/sample/sample.jpg differ diff --git a/scripts/changelog.js b/scripts/changelog.js new file mode 100755 index 00000000..aedcf393 --- /dev/null +++ b/scripts/changelog.js @@ -0,0 +1,16 @@ +#!/usr/bin/env node + +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +/* eslint-env node */ + +/** + * Scripts for generating the changelog before starting the release process. + */ + +require( '@ckeditor/ckeditor5-dev-env' ).generateChangelogForSinglePackage(); diff --git a/scripts/release.js b/scripts/release.js new file mode 100755 index 00000000..3104e7d9 --- /dev/null +++ b/scripts/release.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node + +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +/* eslint-env node */ + +/** + * Scripts for releasing the package. + * + * Before starting the process, "devDependencies" key is removed from package.json. + */ + +const path = require( 'path' ); +const { tools } = require( '@ckeditor/ckeditor5-dev-utils' ); + +const packageJsonPath = path.resolve( 'package.json' ); +const originalPackageJson = require( packageJsonPath ); + +Promise.resolve() + .then( () => { + tools.updateJSONFile( packageJsonPath, json => { + delete json.devDependencies; + + return json; + } ); + } ) + .then( () => { + return require( '@ckeditor/ckeditor5-dev-env' ).releaseRepository(); + } ) + .then( () => { + tools.updateJSONFile( packageJsonPath, () => { + return originalPackageJson; + } ); + } ); + diff --git a/src/ckeditor.js b/src/ckeditor.jsx similarity index 50% rename from src/ckeditor.js rename to src/ckeditor.jsx index 78260ccf..d8372b70 100644 --- a/src/ckeditor.js +++ b/src/ckeditor.jsx @@ -1,23 +1,23 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + import React from 'react'; import PropTypes from 'prop-types'; export default class CKEditor extends React.Component { - constructor( props ) { super( props ); - this.editorInstance = null; - } - - // This component should never be updated by React itself. - shouldComponentUpdate() { - return false; + // After mounting the editor, the variable will contain a reference to created editor. + // @see: https://docs.ckeditor.com/ckeditor5/latest/api/module_core_editor_editor-Editor.html + this.editor = null; } - // Update editor data if data property is changed. - componentWillReceiveProps( newProps ) { - if ( this.editorInstance && newProps.data ) { - this.editorInstance.setData( newProps.data ); + componentDidUpdate() { + if ( this.editor && this.editor.getData() !== this.props.data ) { + this.editor.setData( this.props.data ); } } @@ -33,31 +33,33 @@ export default class CKEditor extends React.Component { // Render
element which will be replaced by CKEditor. render() { - return
( this.domContainer = ref ) }>
; + return ( +
( this.domContainer = ref ) }>
+ ); } _initializeEditor() { this.props.editor .create( this.domContainer, this.props.config ) .then( editor => { - this.editorInstance = editor; + this.editor = editor; - // TODO: Pass data via constructor. - this.editorInstance.setData( this.props.data ); + if ( this.props.data ) { + this.editor.setData( this.props.data ); + } - // TODO: Add example using it. if ( this.props.onInit ) { - this.props.onInit( editor ); + this.props.onInit( this.editor ); } - if ( this.props.onChange ) { - const document = this.editorInstance.model.document; - document.on( 'change', () => { - if ( document.differ.getChanges().length > 0 ) { - this.props.onChange( editor.getData() ); - } - } ); - } + const document = this.editor.model.document; + + document.on( 'change:data', event => { + /* istanbul ignore else */ + if ( this.props.onChange ) { + this.props.onChange( event, editor ); + } + } ); } ) .catch( error => { console.error( error ); @@ -65,8 +67,11 @@ export default class CKEditor extends React.Component { } _destroyEditor() { - if ( this.editorInstance ) { - this.editorInstance.destroy(); + if ( this.editor ) { + this.editor.destroy() + .then( () => { + this.editor = null; + } ); } } } diff --git a/tests/ckeditor-classiceditor.jsx b/tests/ckeditor-classiceditor.jsx new file mode 100644 index 00000000..faaec8bb --- /dev/null +++ b/tests/ckeditor-classiceditor.jsx @@ -0,0 +1,37 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import React from 'react'; +import 'react-dom'; +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; + +import ClassicEditor from '@ckeditor/ckeditor5-build-classic'; +import CKEditor from '../dist/ckeditor'; + +configure( { adapter: new Adapter() } ); + +describe( 'CKEditor Component + ClassicEditor Build', () => { + let wrapper; + + afterEach( () => { + if ( wrapper ) { + wrapper.unmount(); + } + } ); + + it( 'should initialize the ClassicEditor properly', done => { + wrapper = mount( ); + + setTimeout( () => { + const component = wrapper.instance(); + + expect( component.editor ).to.not.be.null; + expect( component.editor.element ).to.not.be.null; + + done(); + } ); + } ); +} ); diff --git a/tests/ckeditor.jsx b/tests/ckeditor.jsx new file mode 100644 index 00000000..53171517 --- /dev/null +++ b/tests/ckeditor.jsx @@ -0,0 +1,292 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import React from 'react'; +import { configure, mount } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; + +import CKEditor from '../src/ckeditor.jsx'; + +configure( { adapter: new Adapter() } ); + +// Mock of data model's document. +const modelDocument = { + on() {} +}; + +// Mock of class that representing a basic, generic editor. +class Editor { + constructor() { + this.model = { + document: modelDocument + } + } + + destroy() { + return Promise.resolve(); + } + + // Implements the `DataApi` interface. + // See: https://docs.ckeditor.com/ckeditor5/latest/api/module_core_editor_utils_dataapimixin-DataApi.html + setData() {} + getData() {} + + static create() { + return Promise.resolve(); + } +} + +describe( 'CKEditor Component', () => { + let sandbox, wrapper; + + beforeEach( () => { + sandbox = sinon.createSandbox(); + sandbox.stub( modelDocument, 'on' ); + } ); + + afterEach( () => { + if ( wrapper ) { + wrapper.unmount(); + } + + sandbox.restore(); + } ); + + it( 'should call "Editor#create()" method during initialization the component', () => { + sandbox.stub( Editor, 'create' ).resolves( new Editor() ); + + wrapper = mount( ); + + expect( Editor.create.calledOnce ).to.be.true; + expect( Editor.create.firstCall.args[ 0 ] ).to.be.an.instanceof( HTMLDivElement ); + expect( Editor.create.firstCall.args[ 1 ] ).to.deep.equal( {} ); + } ); + + it( 'passes configuration object directly to the "Editor#create()" method', () => { + sandbox.stub( Editor, 'create' ).resolves( new Editor() ); + + const editorConfig = { + plugins: [ + function myPlugin() {} + ], + toolbar: { + items: [ 'bold' ] + } + }; + + wrapper = mount( ); + + expect( Editor.create.calledOnce ).to.be.true; + expect( Editor.create.firstCall.args[ 1 ] ).to.deep.equal( editorConfig ); + } ); + + it( 'sets initial data if was specified', done => { + const editorInstance = new Editor(); + + sandbox.stub( Editor, 'create' ).resolves( editorInstance ); + sandbox.stub( editorInstance, 'setData' ); + + wrapper = mount( ); + + setTimeout( () => { + expect( editorInstance.setData.calledOnce ).to.be.true; + expect( editorInstance.setData.firstCall.args[ 0 ] ).to.equal( 'Hello CKEditor 5!' ); + + done(); + } ); + } ); + + it( 'sets editor\'s data if properties have changed and contain the "data" key', done => { + const editorInstance = new Editor(); + + sandbox.stub( Editor, 'create' ).resolves( editorInstance ); + sandbox.stub( editorInstance, 'setData' ); + sandbox.stub( editorInstance, 'getData' ).returns( '

 

' ); + + wrapper = mount( ); + + setTimeout( () => { + wrapper.setProps( { data: '

Foo Bar.

' }); + + expect( editorInstance.setData.calledOnce ).to.be.true; + expect( editorInstance.setData.firstCall.args[ 0 ] ).to.equal( '

Foo Bar.

' ); + + done(); + } ); + } ); + + it( 'does not update the editor\'s data if value under "data" key is equal to editor\'s data', done => { + const editorInstance = new Editor(); + + sandbox.stub( Editor, 'create' ).resolves( editorInstance ); + sandbox.stub( editorInstance, 'setData' ); + sandbox.stub( editorInstance, 'getData' ).returns( '

Foo Bar.

' ); + + wrapper = mount( ); + + setTimeout( () => { + wrapper.setProps( { data: '

Foo Bar.

' }); + + expect( editorInstance.setData.calledOnce ).to.be.false; + + done(); + } ); + } ); + + it( 'does not set editor\'s data if the editor is not ready', () => { + const editorInstance = new Editor(); + + sandbox.stub( Editor, 'create' ).resolves( editorInstance ); + sandbox.stub( editorInstance, 'setData' ); + + wrapper = mount( ); + + const component = wrapper.instance(); + + component.componentDidUpdate( { data: 'Foo' } ); + + expect( component.editor ).to.be.null; + expect( editorInstance.setData.called ).to.be.false; + } ); + + it( 'calls "onInit" callback if specified when the editor is ready to use', done => { + const editorInstance = new Editor(); + const onInit = sandbox.spy(); + + sandbox.stub( Editor, 'create' ).resolves( editorInstance ); + + wrapper = mount( ); + + setTimeout( () => { + expect( onInit.calledOnce ).to.be.true; + expect( onInit.firstCall.args[ 0 ] ).to.equal( editorInstance ); + + done(); + } ); + } ); + + it( 'listens to the editor\'s changes in order to call "onChange" callback', done => { + const editorInstance = new Editor(); + + sandbox.stub( Editor, 'create' ).resolves( editorInstance ); + sandbox.stub( editorInstance, 'getData' ).returns( '

Foo.

' ); + + wrapper = mount( ); + + setTimeout( () => { + expect( modelDocument.on.calledOnce ).to.be.true; + expect( modelDocument.on.firstCall.args[ 0 ] ).to.equal( 'change:data' ); + expect( modelDocument.on.firstCall.args[ 1 ] ).to.be.a( 'function' ); + + done(); + } ); + } ); + + it( 'executes "onChange" callback if specified and editor has changed', done => { + const onChange = sandbox.spy(); + const editorInstance = new Editor(); + + sandbox.stub( Editor, 'create' ).resolves( editorInstance ); + + wrapper = mount( ); + + setTimeout( () => { + const fireChanges = modelDocument.on.firstCall.args[ 1 ]; + const event = { name: 'change:data' }; + + fireChanges( event ); + + expect( onChange.calledOnce ).to.equal( true ); + expect( onChange.firstCall.args[ 0 ] ).to.equal( event ); + expect( onChange.firstCall.args[ 1 ] ).to.equal( editorInstance ); + + done(); + } ); + } ); + + it( 'executes "onChange" callback if it is available in runtime when the editor\'s data has changed', done => { + const onChange = sandbox.spy(); + const editorInstance = new Editor(); + + sandbox.stub( Editor, 'create' ).resolves( editorInstance ); + + wrapper = mount( ); + + setTimeout( () => { + wrapper.setProps( { onChange } ); + + const fireChanges = modelDocument.on.firstCall.args[ 1 ]; + const event = { name: 'change:data' }; + + fireChanges( event ); + + expect( onChange.calledOnce ).to.equal( true ); + expect( onChange.firstCall.args[ 0 ] ).to.equal( event ); + expect( onChange.firstCall.args[ 1 ] ).to.equal( editorInstance ); + + done(); + } ); + } ); + + it( 'displays an error if something went wrong', done => { + const error = new Error( 'Something went wrong.' ); + const consoleErrorStub = sandbox.stub( console, 'error' ); + + sandbox.stub( Editor, 'create' ).rejects( error ); + + wrapper = mount( ); + + setTimeout( () => { + consoleErrorStub.restore(); + + expect( consoleErrorStub.calledOnce ).to.be.true; + expect( consoleErrorStub.firstCall.args[ 0 ] ).to.equal( error ); + + done(); + } ); + } ); + + it( 'should call "Editor#destroy()" method during unmounting the component', done => { + const editorInstance = new Editor(); + + sandbox.stub( Editor, 'create' ).resolves( editorInstance ); + sandbox.stub( editorInstance, 'destroy' ).resolves(); + + wrapper = mount( ); + + setTimeout( () => { + wrapper.unmount(); + wrapper = null; + + expect( editorInstance.destroy.calledOnce ).to.be.true; + + done(); + } ); + } ); + + it( 'should set to "null" the "editor" property inside the component', done => { + const editorInstance = new Editor(); + + sandbox.stub( Editor, 'create' ).resolves( editorInstance ); + sandbox.stub( editorInstance, 'destroy' ).resolves(); + + wrapper = mount( ); + + setTimeout( () => { + const component = wrapper.instance(); + + expect( component.editor ).is.not.null; + + wrapper.unmount(); + wrapper = null; + + setTimeout( () => { + expect( component.editor ).is.null; + + done(); + } ); + } ); + } ); +} ); diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 00000000..e96e4acf --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,75 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +'use strict'; + +/* eslint-env node */ + +const path = require( 'path' ); +const webpack = require( 'webpack' ); +const { bundler } = require( '@ckeditor/ckeditor5-dev-utils' ); +const UglifyJsWebpackPlugin = require( 'uglifyjs-webpack-plugin' ); + +module.exports = { + context: __dirname, + + devtool: 'source-map', + performance: { hints: false }, + externals: { + react: { + root: 'React', + commonjs2: 'react', + commonjs: 'react', + amd: 'react' + } + }, + + entry: path.join( __dirname, 'src', 'ckeditor.jsx' ), + + output: { + library: 'CKEditor', + + path: path.join( __dirname, 'dist' ), + filename: 'ckeditor.js', + libraryTarget: 'umd', + libraryExport: 'default', + + }, + + optimization: { + minimizer: [ + new UglifyJsWebpackPlugin( { + sourceMap: true, + uglifyOptions: { + output: { + // Preserve CKEditor 5 license comments. + comments: /^!/ + } + } + } ) + ] + }, + + plugins: [ + new webpack.BannerPlugin( { + banner: bundler.getLicenseBanner(), + raw: true + } ), + ], + + module: { + rules: [ + { + test: /\.jsx$/, + loader: 'babel-loader', + exclude: /node_modules/, + query: { + compact: false, + presets: [ 'env', 'react' ] + } + } + ] + }, +};