Skip to content
This repository has been archived by the owner on Nov 16, 2017. It is now read-only.

T/10: Provide necessary UI components for Classic Creator #11

Merged
merged 14 commits into from
May 19, 2016
Merged
84 changes: 84 additions & 0 deletions src/iframe/iframeview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

'use strict';

import View from '../view.js';

/**
* The basic iframe view class.
*
* @memberOf ui.iframe
* @extends ui.View
*/
export default class IframeView extends View {
/**
* Creates a new instance of the IframeView.
*
* @param {ui.iframe.IframeModel} [model] (View)Model of this IframeView.
* @param {utils.Locale} [locale] The {@link ckeditor5.Editor#locale editor's locale} instance.
*/
constructor( model, locale ) {
super( model, locale );

this.template = {
tag: 'iframe',
attributes: {
class: [ 'ck-reset-all' ],
// It seems that we need to allow scripts in order to be able to listen to events.
// TODO: Research that. Perhaps the src must be set?
sandbox: 'allow-same-origin allow-scripts'
},
on: {
load: 'loaded'
}
};

/**
* A promise returned by {@link init} since iframe loading may be asynchronous.
*
* **Note**: Listening to `load` in {@link init} makes no sense because at this point
* the element is already in the DOM and the `load` event might already be fired.
*
* See {@link _iframeDeferred}.
*
* @private
* @member {Object} ui.iframe.IframeView
*/
this._iframePromise = new Promise( ( resolve, reject ) => {
/**
* A deferred object used to resolve the iframe promise associated with
* asynchronous loading of `contentDocument`. See {@link _iframePromise}.
*
* @private
* @member {Object} ui.iframe.IframeView
*/
this._iframeDeferred = { resolve, reject };
} );

this.on( 'loaded', () => {
this._iframeDeferred.resolve();
} );
}

/**
* Initializes iframe {@link element} and returns a `Promise` for asynchronous
* child `contentDocument` loading process. See {@link _iframePromise}.
*
* @returns {Promise} A promise which resolves once the iframe `contentDocument` has
* been {@link ui.iframe.IframeView#loaded loaded}.
*/
init() {
super.init();

return this._iframePromise;
}
}

/**
* Fired when the iframe `contentDocument` finished loading.
*
* @event ui.iframe.IframeView#loaded
*/
132 changes: 132 additions & 0 deletions src/stickytoolbar/stickytoolbarview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

'use strict';

import ToolbarView from '../toolbar/toolbarview.js';

/**
* The sticky toolbar view class.
*
* @memberOf ui.toolbar
* @extends ui.toolbarView
*/

export default class StickyToolbarView extends ToolbarView {
constructor( model ) {
super( model );

const bind = this.attributeBinder;

/**
* Indicates whether the toolbar is in the "sticky" state.
*
* @readonly
* @observable
* @member {Boolean} ui.button.StickyToolbarModel#isSticky
*/
this.model.set( 'isSticky', false );

// Toggle class of the toolbar when "sticky" state changes in the model.
this.template.attributes.class.push( bind.if( 'isSticky', 'ck-toolbar_sticky' ) );
}

init() {
super.init();

/**
* A dummy element which visually fills the space as long as the
* actual toolbar is sticky. It prevents flickering of the UI.
*
* @private
* @type {HTMLElement}
* @property _elementPlaceholder
*/
this._elementPlaceholder = document.createElement( 'div' );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where's the placeholder removed?

this._elementPlaceholder.classList.add( 'ck-toolbar__placeholder' );
this.element.parentNode.insertBefore( this._elementPlaceholder, this.element );

// Update sticky state of the toolbar as the window is being scrolled.
this.listenTo( window, 'scroll', () => {
this._checkIfShouldBeSticky();
} );

// Synchronize with `model.isActive` because sticking an inactive toolbar is pointless.
this.model.on( 'change:isActive', ( evt, name, value ) => {
if ( value ) {
this._checkIfShouldBeSticky();
} else {
this._detach();
}
} );
}

/**
* Analyzes the environment to decide whether the toolbar should
* be sticky or not. Then, it uses {@link _stick} and {@link _detach}
* methods to manage the state of the toolbar.
*
* @protected
*/
_checkIfShouldBeSticky() {
const rectElement = this.model.isSticky ?
this._elementPlaceholder : this.element;
const rect = rectElement.getBoundingClientRect();

if ( rect.top < 0 && this.model.isActive ) {
this._stick( rect );
} else {
this._detach();
}
}

/**
* Sticks the toolbar to the top edge of the viewport simulating
* CSS position:sticky. Also see {@link _detach}.
*
* TODO: Possibly replaced by CSS in the future
* http://caniuse.com/#feat=css-sticky
*
* @protected
* @param {Object} regionRect An output of getBoundingClientRect native DOM method.
*/
_stick( regionRect ) {
// Setup placeholder.
Object.assign( this._elementPlaceholder.style, {
display: 'block',
height: regionRect.height + 'px'
} );

// Stick the top region.
Object.assign( this.element.style, {
// Compensate 1px border which is added when becoming "sticky".
width: regionRect.width + 2 + 'px',
marginLeft: -window.scrollX - 1 + 'px'
} );

this.model.isSticky = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the view changing the model. This in fact isn't needed in the model at all. This is view's property (internal at this point).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a thing of a model because of:

this.template.attributes.class.push( bind.if( 'isSticky', 'ck-toolbar_sticky' ) );

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a super bad smell for me unfortunately ;/. If we need to add something to the model only to make bindings work, it's wrong.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PS. Note that isSticky is a totally presentational property.

}

/**
* Detaches the toolbar from the top edge of the viewport.
* See {@link _stick}.
*
* @protected
*/
_detach() {
// Release the placeholder.
Object.assign( this._elementPlaceholder.style, {
display: 'none'
} );

// Detach the top region.
Object.assign( this.element.style, {
width: 'auto',
marginLeft: 'auto'
} );

this.model.isSticky = false;
}
}
2 changes: 1 addition & 1 deletion src/toolbar/toolbarview.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default class ToolbarView extends View {
this.template = {
tag: 'div',
attributes: {
class: [ 'ck-toolbar' ]
class: [ 'ck-reset ck-toolbar' ]
}
};

Expand Down
69 changes: 69 additions & 0 deletions tests/iframe/iframeview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/* bender-tags: ui, iframe */

'use strict';

import IframeView from '/ckeditor5/ui/iframe/iframeview.js';
import Model from '/ckeditor5/ui/model.js';

describe( 'IframeView', () => {
let model, view;

beforeEach( () => {
model = new Model( {
width: 100,
height: 200
} );

view = new IframeView( model );

view.init();
} );

describe( 'constructor', () => {
it( 'creates view element from the template', () => {
expect( view.element.classList.contains( 'ck-reset-all' ) ).to.be.true;
expect( view.element.attributes.getNamedItem( 'sandbox' ).value ).to.equal( 'allow-same-origin allow-scripts' );
} );
} );

describe( 'init', () => {
it( 'returns promise', () => {
view = new IframeView( model );

expect( view.init() ).to.be.an.instanceof( Promise );
} );

it( 'returns promise which is resolved when iframe finished loading', () => {
view = new IframeView( model );

const promise = view.init().then( () => {
expect( view.element.contentDocument.readyState ).to.equal( 'complete' );
} );

// Moving iframe into DOM trigger creation of a document inside iframe.
document.body.appendChild( view.element );

return promise;
} );
} );

describe( 'loaded event', () => {
it( 'is fired when frame finished loading', ( done ) => {
view = new IframeView( model );

view.on( 'loaded', () => {
done();
} );

view.init();

// Moving iframe into DOM trigger creation of a document inside iframe.
document.body.appendChild( view.element );
} );
} );
} );
11 changes: 11 additions & 0 deletions theme/components/stickytoolbar.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
// For licensing, see LICENSE.md or http://ckeditor.com/license

@include ck-editor {
.ck-toolbar.ck-toolbar_sticky {
position: fixed;
top: 0;
border: 1px solid ck-border-color();
background: ck-color( 'foreground' );
}
}
4 changes: 4 additions & 0 deletions theme/theme.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
// For licensing, see LICENSE.md or http://ckeditor.com/license

@import 'components/stickytoolbar';