Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

t/44: Allowed a configuration to CKEditorInspector#attach() to attach the i… #46

Merged
merged 14 commits into from
Sep 20, 2019
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 59 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,71 @@ Include the script to load the inspector:
<script src="path/to/inspector.js"></script>
```

Call `CKEditorInspector.attach( name, editor )` when editor instance is ready:
Call `CKEditorInspector.attach( editor )` when editor instance is ready:

```js
ClassicEditor
.create( ... )
.then( editor => {
CKEditorInspector.attach( 'editor-name', editor );
} )
.catch( error => {
console.error( error );
} );
.create( ... )
.then( editor => {
CKEditorInspector.attach( editor );
} )
.catch( error => {
console.error( error );
} );
```

**Note**: You can attach multiple editors to the inspector. Select the editor instance in the drop–down inside the inspector panel to switch context.
**Note**: You can attach to multiple editors under unique names at a time. Then you can select the editor instance in the drop–down inside the inspector panel to switch context.

Call `CKEditorInspector.detach( name )` to detach an instance from the inspector.
```js
CKEditorInspector.attach( {
'header-editor': editor1,
'footer-editor': editor2,
// ...
} );
```

Call `CKEditorInspector.detach( name )` to detach the inspector from an editor instance.

**Tip**: `CKEditorInspector.attach()` returns the generated name of the editor if it was not provided.

```js
// Attach the inspector to two editor instances:
const generatedName = CKEditorInspector.attach( editor1 );
CKEditorInspector.attach( { myEditor: editor2 } );

// ...

// Detach from the instances:
CKEditorInspector.detach( generatedName );
CKEditorInspector.detach( 'myEditor' );
```

### Configuration

You can pass configuration options to the `CKEditorInspector.attach()` method as the last argument:

```js
CKEditorInspector.attach( editor, {
// configuration options
} );

CKEditorInspector.attach( { 'editor-name': editor }, {
// configuration options
} );
```

#### `isCollapsed`

To attach the inspector with a collapsed UI, use the `options.isCollapsed` option.

**Note**: This option works when `CKEditorInspector.attach()` is called for the first time only.

```js
CKEditorInspector.attach( { 'editor-name': editor }, {
// Attach the inspector to the "editor" but the UI will be collapsed.
isCollapsed: true
} );
```

## Compatibility

Expand Down
2 changes: 1 addition & 1 deletion sample/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ <h2>Second editor</h2>
.then( editor => {
window.firstEditor = editor;
document.body.insertBefore( editor.ui.view.toolbar.element, editor.ui.getEditableElement() );
CKEditorInspector.attach( 'first-editor', editor );
CKEditorInspector.attach( { 'first-editor': editor } );
} )
.catch( error => {
console.error( error );
Expand Down
152 changes: 108 additions & 44 deletions src/ckeditorinspector.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,12 @@ import ReactDOM from 'react-dom';

import InspectorUI from './components/ui';
import Logger from './logger';
import { normalizeArguments } from './utils';
import './ckeditorinspector.css';

// From changelog -> webpack.
window.CKEDITOR_INSPECTOR_VERSION = CKEDITOR_INSPECTOR_VERSION;

const container = document.createElement( 'div' );
container.className = 'ck-inspector-wrapper';

let unnamedEditorCount = 0;

export default class CKEditorInspector {
/**
* Attaches the inspector to an editor instance.
Expand All @@ -28,69 +24,137 @@ export default class CKEditorInspector {
* .create( ... )
* .then( editor => {
* CKEditorInspector.attach( editor );
*
* // Alternatively:
* // CKEditorInspector.attach( 'my-editor', editor );
* } )
* .catch( error => {
* console.error( error );
* } );
*
* @param {Editor|String} editorOrName When an unique string is provided, the editor will be listed in the inspector
* under a name (the instance passed as a second argument). If an editor instance is passed, the editor with be
* attached and assigned a generated name.
* @param {Editor} [editor] An instance of the editor, if the first argument was specified as a string.
* @returns {String} The unique name of the editor in the inspector. Useful when using `CKEditorInspector.detach()`.
* **Note:** You can attach to multiple editors at a time under unique names:
*
* CKEditorInspector.attach( {
* 'header-editor': editor1,
* 'footer-editor': editor2,
* // ...
* } );
*
* **Note:** You can pass global configuration options when attaching:
*
* CKEditorInspector.attach( editor, { option: 'value', ... } );
* CKEditorInspector.attach( {
* 'header-editor': editor1,
* 'footer-editor': editor2
* }, { option: 'value', ... } );
*
* @param {Editor|Object} editorOrEditors If an editor instance is passed, the inspect will attach to the editor
* with an auto–generated name. It is possible to pass an object with `name: instance` pairs to attach to
* multiple editors at a time with unique names.
* @param {CKEditorInspectorConfig} [options] An object of global configuration options controlling the
* behavior of the inspector.
* @returns {Array.<String>} Names of the editors the inspector attached to. Useful when using `CKEditorInspector.detach()`
* with generated editor names.
*/
static attach( editorOrName, editor ) {
let name, instance;

if ( typeof editorOrName === 'string' ) {
name = editorOrName;
instance = editor;
} else {
name = `editor-${ ++unnamedEditorCount }`;
instance = editorOrName;
}
static attach( ...args ) {
const { editors, options } = normalizeArguments( args );

Logger.group( '%cAttached the inspector to a CKEditor 5 instance. To learn more, visit https://ckeditor.com/docs/ckeditor5.',
'font-weight: bold;' );
Logger.log( `Editor instance "${ name }"`, instance );
Logger.groupEnd();
for ( const editorName in editors ) {
const editorInstance = editors[ editorName ];

CKEditorInspector._editors.set( name, instance );
Logger.group( '%cAttached the inspector to a CKEditor 5 instance. To learn more, visit https://ckeditor.com/docs/ckeditor5.',
'font-weight: bold;' );
Logger.log( `Editor instance "${ editorName }"`, editorInstance );
Logger.groupEnd();

instance.on( 'destroy', () => {
CKEditorInspector.detach( name );
} );
CKEditorInspector._editors.set( editorName, editorInstance );

if ( !container.parentNode ) {
document.body.appendChild( container );
editorInstance.on( 'destroy', () => {
CKEditorInspector.detach( editorName );
} );

ReactDOM.render(
<InspectorUI
ref={CKEditorInspector._inspectorRef}
editors={CKEditorInspector._editors}
/>,
container );
CKEditorInspector._mount( options );
CKEditorInspector._updateEditorsState();
}

CKEditorInspector._updateState();

return name;
return Object.keys( editors );
}

/**
* Detaches the inspector from an editor instance.
*
* CKEditorInspector.attach( { 'my-editor': editor } );
*
* // The inspector will no longer inspect the "editor".
* CKEditorInspector.detach( 'my-editor' );
*
* @param {String} string Name of the editor to detach.
*/
static detach( name ) {
CKEditorInspector._editors.delete( name );
CKEditorInspector._updateState();
CKEditorInspector._updateEditorsState();
}

static _updateState() {
/**
* Destroys the entire inspector application and removes it from DOM.
*/
static destroy() {
if ( !CKEditorInspector._wrapper ) {
return;
}

ReactDOM.unmountComponentAtNode( CKEditorInspector._wrapper );
CKEditorInspector._editors.clear();
CKEditorInspector._wrapper.remove();
CKEditorInspector._wrapper = null;
}

static _updateEditorsState() {
// Don't update state if the application was destroy()ed.
ma2ciek marked this conversation as resolved.
Show resolved Hide resolved
if ( !CKEditorInspector._isMounted ) {
return;
}

CKEditorInspector._inspectorRef.current.setState( {
editors: CKEditorInspector._editors
} );
}

static _mount( options ) {
if ( CKEditorInspector._wrapper ) {
return;
}

const container = CKEditorInspector._wrapper = document.createElement( 'div' );
container.className = 'ck-inspector-wrapper';
document.body.appendChild( container );

ReactDOM.render(
<InspectorUI
ref={CKEditorInspector._inspectorRef}
editors={CKEditorInspector._editors}
isCollapsed={options.isCollapsed}
/>,
container );
}

static get _isMounted() {
return !!CKEditorInspector._inspectorRef.current;
}
}

CKEditorInspector._editors = new Map();
CKEditorInspector._inspectorRef = React.createRef();
CKEditorInspector._wrapper = null;

/**
* The configuration options of the inspector.
*
* @interface CKEditorInspectorConfig
*/

/**
* Controls the initial collapsed state of the inspector. Allows attaching to an editor instance without
* expanding the UI.
*
* **Note**: Works when `attach()` is called for the first time only.
*
* @member {Boolean} CKEditorInspectorConfig#isCollapsed
*/
6 changes: 5 additions & 1 deletion src/components/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ export default class InspectorUI extends Component {

const height = StorageManager.get( LOCAL_STORAGE_INSPECTOR_HEIGHT ) || INSPECTOR_DEFAULT_HEIGHT;

// The collapsed state can be either configured by passing an option to `attach()`
// or retrieved from the last "session".
const isCollapsed = this.props.isCollapsed === true || StorageManager.get( LOCAL_STORAGE_IS_COLLAPSED ) === 'true';

this.state = {
isCollapsed: StorageManager.get( LOCAL_STORAGE_IS_COLLAPSED ) === 'true',
isCollapsed,
height,
editors: null,
currentEditorName: null,
Expand Down
4 changes: 4 additions & 0 deletions src/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,8 @@ export default class Logger {
static log( ...args ) {
console.log( ...args );
}

static warn( ...args ) {
ma2ciek marked this conversation as resolved.
Show resolved Hide resolved
console.warn( ...args );
}
}
51 changes: 51 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

import Logger from './logger';

let unnamedEditorCount = 0;

export function normalizeArguments( args ) {
const normalized = {
editors: {},
options: {}
};

// Deprecated // attach( 'name', editor );
if ( typeof args[ 0 ] === 'string' ) {
Logger.warn(
`[CKEditorInspector] The CKEditorInspector.attach( '${ args[ 0 ] }', editor ) syntax has been deprecated ` +
'and will be removed in the near future. To pass a name of an editor instance, use ' +
`CKEditorInspector.attach( { '${ args[ 0 ] }': editor } ) instead.`
);

normalized.editors[ args[ 0 ] ] = args[ 1 ];
} else {
// attach( editor );
if ( isEditorInstance( args[ 0 ] ) ) {
normalized.editors[ getNextEditorName() ] = args[ 0 ];
}
// attach( { foo: editor1, bar: editor2, ... } );
else {
for ( const name in args[ 0 ] ) {
normalized.editors[ name ] = args[ 0 ][ name ];
}
}

// attach( ..., { options } );
normalized.options = args[ 1 ] || normalized.options;
}

return normalized;
}

function getNextEditorName() {
return `editor-${ ++unnamedEditorCount }`;
}

function isEditorInstance( arg ) {
// Quack! 🦆
return !!arg.model;
}
Loading