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

[WIP] Written the UI layer of the SourceEditing plugin using ckeditor… #12063

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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
76 changes: 48 additions & 28 deletions packages/ckeditor5-source-editing/src/sourceediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@

import { Plugin, PendingActions } from 'ckeditor5/src/core';
import { ButtonView } from 'ckeditor5/src/ui';
import { createElement, ElementReplacer } from 'ckeditor5/src/utils';
import { ElementReplacer } from 'ckeditor5/src/utils';
import { formatHtml } from './utils/formathtml';

import '../theme/sourceediting.css';

import sourceEditingIcon from '../theme/icons/source-editing.svg';
import SourceEditingWrapperView from './ui/sourceeditingwrapperview';

const COMMAND_FORCE_DISABLE_ID = 'SourceEditingMode';

Expand Down Expand Up @@ -71,7 +72,7 @@ export default class SourceEditing extends Plugin {
* Maps all root names to wrapper elements containing the document source.
*
* @private
* @member {Map.<String,HTMLElement>}
* @member {Map.<String,SourceEditingWrapperView>}
*/
this._replacedRoots = new Map();

Expand All @@ -82,6 +83,11 @@ export default class SourceEditing extends Plugin {
* @member {Map.<String,String>}
*/
this._dataFromRoots = new Map();

/**
* TODO
*/
this._rootNamesToWrapperViews = new Map();
}

/**
Expand Down Expand Up @@ -195,6 +201,13 @@ export default class SourceEditing extends Plugin {
}
}

/**
* TODO
*/
destroy() {
this._rootNamesToWrapperViews.forEach( wrapperView => wrapperView.destroy() );
}

/**
* Creates source editing wrappers that replace each editing root. Each wrapper contains the document source from the corresponding
* root.
Expand Down Expand Up @@ -224,37 +237,31 @@ export default class SourceEditing extends Plugin {
// main root, but this code may help understand and use this feature in external integrations.
for ( const [ rootName, domRootElement ] of editingView.domRoots ) {
const data = formatSource( editor.data.get( { rootName } ) );
let wrapperView;

const domSourceEditingElementTextarea = createElement( domRootElement.ownerDocument, 'textarea', {
rows: '1',
'aria-label': 'Source code editing area'
} );
if ( !this._rootNamesToWrapperViews.has( rootName ) ) {
wrapperView = new SourceEditingWrapperView( editor.locale );
wrapperView.render();

const domSourceEditingElementWrapper = createElement( domRootElement.ownerDocument, 'div', {
class: 'ck-source-editing-area',
'data-value': data
}, [ domSourceEditingElementTextarea ] );
this._rootNamesToWrapperViews.set( rootName, wrapperView );
} else {
wrapperView = this._rootNamesToWrapperViews.get( rootName );
}

domSourceEditingElementTextarea.value = data;
wrapperView.value = data;

// Setting a value to textarea moves the input cursor to the end. We want the selection at the beginning.
domSourceEditingElementTextarea.setSelectionRange( 0, 0 );

// Bind the textarea's value to the wrapper's `data-value` property. Each change of the textarea's value updates the
// wrapper's `data-value` property.
domSourceEditingElementTextarea.addEventListener( 'input', () => {
domSourceEditingElementWrapper.dataset.value = domSourceEditingElementTextarea.value;
} );
wrapperView.textareaView.setSelectionRange( 0, 0 );

editingView.change( writer => {
const viewRoot = editingView.document.getRoot( rootName );

writer.addClass( 'ck-hidden', viewRoot );
} );

this._replacedRoots.set( rootName, domSourceEditingElementWrapper );
this._replacedRoots.set( rootName, wrapperView );

this._elementReplacer.replace( domRootElement, domSourceEditingElementWrapper );
this._elementReplacer.replace( domRootElement, wrapperView.element );

this._dataFromRoots.set( rootName, data );
}
Expand Down Expand Up @@ -296,9 +303,9 @@ export default class SourceEditing extends Plugin {
const editor = this.editor;
const data = {};

for ( const [ rootName, domSourceEditingElementWrapper ] of this._replacedRoots ) {
for ( const [ rootName, wrapperView ] of this._replacedRoots ) {
const oldData = this._dataFromRoots.get( rootName );
const newData = domSourceEditingElementWrapper.dataset.value;
const newData = wrapperView.value;

// Do not set the data unless some changes have been made in the meantime.
// This prevents empty undo steps after switching to the normal editor.
Expand All @@ -318,11 +325,24 @@ export default class SourceEditing extends Plugin {
* @private
*/
_focusSourceEditing() {
const [ domSourceEditingElementWrapper ] = this._replacedRoots.values();
const editor = this.editor;

// Keep the editor UI focused while the editing goes in the <textarea>.
// editor.ui.focusTracker.add( textarea );

// The FocusObserver was disabled by View.render() while the DOM root was getting hidden and the replacer
// revealed the textarea. So it couldn't notice that the DOM root got blurred in the process.
// Let's sync this state manually here because otherwise Renderer will attempt to render selection
// in an invisible DOM root.
editor.editing.view.document.isFocused = false;

const textarea = domSourceEditingElementWrapper.querySelector( 'textarea' );
for ( const [ , wrapperView ] of this._rootNamesToWrapperViews ) {
if ( wrapperView.element.parentNode ) {
wrapperView.textareaView.focus();

textarea.focus();
break;
}
}
}

/**
Expand Down Expand Up @@ -361,9 +381,9 @@ export default class SourceEditing extends Plugin {
return;
}

for ( const [ , domSourceEditingElementWrapper ] of this._replacedRoots ) {
domSourceEditingElementWrapper.querySelector( 'textarea' ).readOnly = isReadOnly;
}
this._rootNamesToWrapperViews.forEach( wrapperView => {
wrapperView.textareaView.isReadOnly = isReadOnly;
} );
}

/**
Expand Down
113 changes: 113 additions & 0 deletions packages/ckeditor5-source-editing/src/ui/sourceeditingwrapperview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/**
* @module TODO
*/

import TextareaView from './textareaview';
import { View } from 'ckeditor5/src/ui';

/**
* TODO
*
* @private
* @extends module:ui/view~View
*/
export default class SourceEditingWrapperView extends View {
/**
* @inheritDoc
*/
constructor( locale ) {
super( locale );

const bind = this.bindTemplate;

/**
* TODO
*/
this.textareaView = new TextareaView( locale, {
rows: 1,
'aria-label': 'Source code editing area'
} );

/**
* TODO
*/
this.set( 'value', '' );

// TODO
this.on( 'change:value', () => {
this.textareaView.value = this.value;
} );

// Bind the textarea's value to the wrapper's `data-value` property.
// Each change of the textarea's value updates the wrapper's `data-value` property.
this.textareaView.on( 'input', () => {
this.value = this.textareaView.element.value;
} );

this.setTemplate( {
tag: 'div',
attributes: {
class: [
'ck',
'ck-source-editing-area'
],
// TODO: Explain this magic because this purely presentational to make
// the <textarea> auto-grow.
'data-value': bind.to( 'value' )
},
children: [
this.textareaView
]
} );
}

/**
* @inheritDoc
*/
render() {
super.render();

this._setDomElementValue( this.value );

// Bind `this.value` to the DOM element's value.
this.on( 'change:value', ( evt, name, value ) => {
this._setDomElementValue( value );
} );
}

/**
* TODO
*/
focus() {
this.element.focus();
}

/**
* TODO
* @param {...any} args
*/
setSelectionRange( ...args ) {
this.element.setSelectionRange( ...args );
}

/**
* Sets the `value` property of the {@link #element DOM element} on demand.
*
* @private
*/
_setDomElementValue( value ) {
this.element.value = ( !value && value !== 0 ) ? '' : value;
}

/**
* Fired when the user types in the textarea. Corresponds to the native
* DOM `input` event.
*
* @event input
*/
}
104 changes: 104 additions & 0 deletions packages/ckeditor5-source-editing/src/ui/textareaview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/**
* @module TODO
*/

import { View } from 'ckeditor5/src/ui';

/**
* The base textarea view class.
*
* @private
* @extends module:ui/view~View
*/
export default class TextareaView extends View {
/**
* @inheritDoc
*/
constructor( locale, options = {} ) {
super( locale );

const bind = this.bindTemplate;

/**
* The value of the textarea.
*
* @observable
* @member {String} #value
*/
this.set( 'value', '' );

/**
* Controls whether the textarea view is in read-only mode.
*
* @observable
* @member {Boolean} #isReadOnly
*/
this.set( 'isReadOnly', false );

this.setTemplate( {
tag: 'textarea',
attributes: {
class: [
'ck',
'ck-textarea'
],
readonly: bind.to( 'isReadOnly' ),
rows: options.rows,
'aria-label': options[ 'aria-label' ]
},
on: {
input: bind.to( 'input' )
}
} );
}

/**
* @inheritDoc
*/
render() {
super.render();

this._setDomElementValue( this.value );

// Bind `this.value` to the DOM element's value.
this.on( 'change:value', ( evt, name, value ) => {
this._setDomElementValue( value );
} );
}

/**
* TODO
*/
focus() {
this.element.focus();
}

/**
* TODO
* @param {...any} args
*/
setSelectionRange( ...args ) {
this.element.setSelectionRange( ...args );
}

/**
* Sets the `value` property of the {@link #element DOM element} on demand.
*
* @private
*/
_setDomElementValue( value ) {
this.element.value = ( !value && value !== 0 ) ? '' : value;
}

/**
* Fired when the user types in the textarea. Corresponds to the native
* DOM `input` event.
*
* @event input
*/
}
4 changes: 2 additions & 2 deletions packages/ckeditor5-source-editing/tests/sourceediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ describe( 'SourceEditing', () => {
const wrapper = domRoot.nextSibling;

expect( wrapper.nodeName ).to.equal( 'DIV' );
expect( wrapper.className ).to.equal( 'ck-source-editing-area' );
expect( wrapper.className ).to.equal( 'ck ck-source-editing-area' );
expect( wrapper.dataset.value ).to.equal(
'<p>\n' +
' Foo\n' +
Expand Down Expand Up @@ -342,7 +342,7 @@ describe( 'SourceEditing', () => {
const domRoot = editor.editing.view.getDomRoot();
const wrapper = domRoot.nextSibling;

expect( plugin._replacedRoots.get( 'main' ) ).to.equal( wrapper );
expect( plugin._replacedRoots.get( 'main' ).element ).to.equal( wrapper );
} );

it( 'should remember document data from roots', () => {
Expand Down