Skip to content

Instantly share code, notes, and snippets.

@fredck
Created May 8, 2020 23:02
Show Gist options
  • Save fredck/a80c0b86ed53d3da3776c55ebb3e0e19 to your computer and use it in GitHub Desktop.
Save fredck/a80c0b86ed53d3da3776c55ebb3e0e19 to your computer and use it in GitHub Desktop.
Introduces `editor.data.view` and the "Live HTML Data Processor"
/*
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import View from '@ckeditor/ckeditor5-engine/src/view/view';
import ContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement';
import RootEditableElement from '@ckeditor/ckeditor5-engine/src/view/rooteditableelement';
import ViewDocumentFragment from '@ckeditor/ckeditor5-engine/src/view/documentfragment';
import {
clearAttributes, convertCollapsedSelection,
convertRangeSelection,
remove
} from '@ckeditor/ckeditor5-engine/src/conversion/downcasthelpers';
export default class DataView extends Plugin {
constructor( editor ) {
super( editor );
const data = editor.data;
/**
* Data view controller.
*
* @readonly
* @memberOf Editor
* @member {View} #view
*/
const view = data.view = new View( data.stylesProcessor );
// Minor improvements that don't hurt.
{
// We don't need observers in this view.
view._observers = new Map();
// We don't care about the selection.
view.stopListening( view.document.selection, 'change' );
}
// Destroy the view when the data controller is destroyed.
{
data.decorate( 'destroy' );
data.once( 'destroy', () => view.destroy() );
}
// View updating. It's highly inspired by @ckeditor/ckeditor5-engine/src/controller/editingcontroller.
{
const model = data.model;
const downcastDispatcher = data.downcastDispatcher;
const { document, markers } = model;
// Disable rendering while model changes are happening (they could happen during rendering as well).
this.listenTo( model, '_beforeChanges', () => view._disableRendering( true ), { priority: 'highest' } );
this.listenTo( model, '_afterChanges', () => view._disableRendering( false ), { priority: 'lowest' } );
// Whenever the model document is changed, convert those changes to the view.
// Do it on 'low' priority, so changes are converted after other listeners did their job.
this.listenTo( document, 'change:data', () => {
view.change( writer => {
downcastDispatcher.convertChanges( document.differ, markers, writer );
} );
}, { priority: 'low' } );
downcastDispatcher.on( 'remove', remove(), { priority: 'low' } );
// Binds {@link module:engine/view/document~Document#roots view roots collection} to
// {@link module:engine/model/document~Document#roots model roots collection} so creating
// model root automatically creates corresponding view root.
view.document.roots.bindTo( model.document.roots ).using( root => {
// $graveyard is a special root that has no reflection in the view.
if ( root.rootName === '$graveyard' ) {
return null;
}
const viewRoot = new DataViewRootElement( view.document, root.name );
viewRoot.rootName = root.rootName;
data.mapper.bindElements( root, viewRoot );
return viewRoot;
} );
}
// Override data.stringify() as we now made the view direcly manipulable.
{
const originalStringify = data.stringify;
data.stringify = modelElementOrFragment => {
if ( data.processor.rootToData && modelElementOrFragment.is( 'rootElement' ) ) {
const stringified = data.processor.rootToData( modelElementOrFragment.rootName );
if ( stringified !== false ) {
return stringified;
}
}
return originalStringify.call( this, modelElementOrFragment );
};
}
}
}
/**
* A view root element. It targets the Data Controller, where no editing is expected.
*
* It emulates RootEditableElement, expect that it based on ContainerElement instead of EditableElement.
*/
export class DataViewRootElement extends ContainerElement {
constructor( document, name ) {
super( document, name );
this.rootName = 'main';
}
get isReadOnly() {
return true;
}
}
// Methods and properties we want to copy from RootEditableElement.
[ 'is', 'rootName', '_name' ].forEach( property => {
Object.defineProperty( DataViewRootElement.prototype, property,
Object.getOwnPropertyDescriptor( RootEditableElement.prototype, property ) );
} );
/*
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import DataView from './dataview';
import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor';
export default class LiveHtmlDataProcessor extends Plugin {
static get requires() {
return [ DataView ];
}
static get pluginName() {
return 'LiveHtmlDataProcessor';
}
constructor( editor ) {
super( editor );
this.element = document.createElement( 'div' );
editor.data.view.attachDomRoot( this.element );
}
rootToData( rootName ) {
const element = this.editor.data.view.domRoots.get( rootName );
if ( element && element === this.element ) {
// Here goes the performance boost -> getData() = element.innerHTML
return element.innerHTML;
}
return false;
}
}
// Methods and properties we want to copy from RootEditableElement.
[ 'toData', 'toView' ].forEach( property => {
Object.defineProperty( LiveHtmlDataProcessor.prototype, property,
Object.getOwnPropertyDescriptor( HtmlDataProcessor.prototype, property ) );
} );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment