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

Detect if mention panel goes beyond window borders #4266

Merged
merged 55 commits into from
Dec 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
d3ef1ae
Detect if mention panel goes beyond window borders.
hub33k Sep 8, 2020
0e031e7
Tweak checking panel position.
hub33k Sep 9, 2020
a9a4da9
Tests: add manual test.
hub33k Sep 9, 2020
8da4298
Change way of getting window height.
hub33k Sep 10, 2020
49d6621
Tests: add unit test.
hub33k Sep 10, 2020
4b4e9d3
Tests: rework manual test.
hub33k Sep 16, 2020
e0cc6c8
Detect if panel goes beyond horizontal window borders.
hub33k Sep 16, 2020
6144e9b
Add right position to caret (getViewPosition function).
hub33k Sep 29, 2020
f5a44c4
Tests: fix manual test (replace margin with padding on body).
hub33k Sep 29, 2020
3ac2a6a
Tweak condition for checking right window border.
hub33k Sep 29, 2020
91ddedc
Tests: rework manual test.
hub33k Sep 30, 2020
ac79987
Tests: add manual test (RTL version).
hub33k Sep 30, 2020
a264c4a
Add graphic comments.
hub33k Sep 30, 2020
d8f44b7
Remove unneeded blank line.
hub33k Sep 30, 2020
881e568
Adjust condition when view goes beyond bottom window border.
hub33k Sep 30, 2020
9ad3aaf
Tests: minor rewording in manual test.
hub33k Oct 8, 2020
bd4f5a9
Tests: minor rewording in manual test.
hub33k Oct 8, 2020
9bec00a
Tests: minor rewording in manual test.
hub33k Oct 8, 2020
cdecaf8
Tests: minor rewording in manual test.
hub33k Oct 8, 2020
b59143a
Add note about rect.right that was introduced in 4.16.0.
hub33k Oct 8, 2020
6af317b
Tests: test rect.right in autocomplete/view.
hub33k Oct 14, 2020
2b95932
Change tabs to spaces in viewport schemas.
Dumluregn Nov 5, 2020
256c92e
Refactor to use 'getViewPaneSize()' method.
Dumluregn Nov 6, 2020
db16968
Smoothly adjust the position to the right.
Dumluregn Nov 6, 2020
6fb66d2
Fix vertical positioning.
Dumluregn Nov 6, 2020
b4d0733
Refactor to call getWindow() only once.
Dumluregn Nov 6, 2020
c63b513
Adjust horizontal position based on the left rect, not right.
Dumluregn Nov 6, 2020
d96d292
Remove rect.top from getViewPosition() returns.
Dumluregn Nov 6, 2020
e1c2c2c
Fix comments describing the positioning.
Dumluregn Nov 9, 2020
2f19fb3
Revert "Tests: test rect.right in autocomplete/view."
Dumluregn Nov 9, 2020
da13ddf
Remove irrelevant manual test.
Dumluregn Nov 9, 2020
0662ec9
Add tests for all window borders.
Dumluregn Nov 9, 2020
cde377e
Fix previously existing unit tests.
Dumluregn Nov 9, 2020
080ab03
Improve manual tests wording.
Dumluregn Nov 9, 2020
b47100b
Add integration tests with mentions and emoji plugins.
Dumluregn Nov 9, 2020
7e35d78
Improve manual tests text layout.
Dumluregn Nov 10, 2020
1a4334f
Add note about ignoring part of manual tests on IE 10 and older.
Dumluregn Nov 10, 2020
682561a
Rename variable.
Dumluregn Nov 18, 2020
2249703
Remove note about old IEs from emoji test.
Dumluregn Nov 18, 2020
217d708
Fix integration tests paths.
Dumluregn Nov 18, 2020
91f652a
Don't revert the view position if it exceeds top browser viewport.
Dumluregn Nov 19, 2020
649a3f4
Add unit test for the last case.
Dumluregn Nov 19, 2020
a880d89
Add manual test using resizer.
Dumluregn Nov 19, 2020
f529d03
Rename previous manual tests.
Dumluregn Nov 19, 2020
c308091
Improve test on mobile.
Dumluregn Nov 19, 2020
f1ba3d2
Ignore manual tests on mobile devices.
Dumluregn Nov 20, 2020
5469b4a
Rename unit test.
Dumluregn Nov 20, 2020
30c01d5
Move calculating the horizontal position to the function.
Dumluregn Nov 25, 2020
52be8a8
Extract the editorViewportRect calculations.
Dumluregn Nov 25, 2020
0a0206c
Extract calculating the vertical position.
Dumluregn Nov 25, 2020
98f52f8
Refactor variables names and scoping.
Dumluregn Nov 25, 2020
201f89e
Use objects in functions signatures.
Dumluregn Nov 25, 2020
690131c
Rename a few variables according to the review comments.
Dumluregn Dec 7, 2020
b01f559
Remove a comment from manual test.
Dumluregn Dec 7, 2020
fd3c892
Changelog entry. [skip ci]
f1ames Dec 8, 2020
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
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ New Features:

* [#2800](https://github.com/ckeditor/ckeditor4/issues/2800): Unsupported image formats are now gracefully handled by the [Paste from Word](https://ckeditor.com/cke4/addon/pastefromword) plugin on paste, additionally showing descriptive error messages.
* [#2800](https://github.com/ckeditor/ckeditor4/issues/2800): Unsupported image formats are now gracefully handled by the [Paste from LibreOffice](https://ckeditor.com/cke4/addon/pastefromlibreoffice) plugin on paste, additionally showing descriptive error messages.
* [#3582](https://github.com/ckeditor/ckeditor4/issues/3582): Introduced smart positioning of [Autocomplete](https://ckeditor.com/cke4/addon/autocomplete) panel used by [Mentions](https://ckeditor.com/cke4/addon/mentions) and [Emoji](https://ckeditor.com/cke4/addon/emoji) plugins. The panel will now be additionally positioned related to browser viewport to be always fully visible.

Fixed Issues:

Expand Down
233 changes: 152 additions & 81 deletions plugins/autocomplete/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -857,94 +857,165 @@
* For example, the position of the bottom end of the caret.
*/
setPosition: function( rect ) {
var editor = this.editor,
viewHeight = this.element.getSize( 'height' ),
editable = editor.editable(),
// Bounding rect where the view should fit (visible editor viewport).
editorViewportRect;
var documentWindow = this.element.getWindow(),
windowRect = documentWindow.getViewPaneSize(),
top = getVerticalPosition( {
editorViewportRect: getEditorViewportRect( this.editor ),
caretRect: rect,
viewHeight: this.element.getSize( 'height' ),
scrollPositionY: documentWindow.getScrollPosition().y,
windowHeight: windowRect.height
} ),
left = getHorizontalPosition( {
leftPosition: rect.left,
viewWidth: this.element.getSize( 'width' ),
windowWidth: windowRect.width
} );

// iOS classic editor has different viewport element (#1910).
if ( CKEDITOR.env.iOS && !editable.isInline() ) {
editorViewportRect = iOSViewportElement( editor ).getClientRect( true );
} else {
editorViewportRect = editable.isInline() ? editable.getClientRect( true ) : editor.window.getFrame().getClientRect( true );
}
this.element.setStyles( {
left: left + 'px',
top: top + 'px'
} );

// How much space is there for the view above and below the specified rect.
var spaceAbove = rect.top - editorViewportRect.top,
spaceBelow = editorViewportRect.bottom - rect.bottom,
top;

// As a default, keep the view inside the editor viewport.
// +---------------------------------------------+
// | editor viewport |
// | |
// | |
// | |
// | █ - caret position |
// | +--------------+ |
// | | view | |
// | +--------------+ |
// | |
// | |
// +---------------------------------------------+
top = rect.top < editorViewportRect.top ? editorViewportRect.top : Math.min( editorViewportRect.bottom, rect.bottom );

// If the view doesn't fit below the caret position and fits above, set it there.
// This means that the position below the caret is preferred.
// +---------------------------------------------+
// | |
// | editor viewport |
// | +--------------+ |
// | | | |
// | | view | |
// | | | |
// | +--------------+ |
// | █ - caret position |
// | |
// +---------------------------------------------+
if ( viewHeight > spaceBelow && viewHeight < spaceAbove ) {
top = rect.top - viewHeight;
}
function getVerticalPosition( options ) {
var editorViewportRect = options.editorViewportRect,
caretRect = options.caretRect,
viewHeight = options.viewHeight,
scrollPositionY = options.scrollPositionY,
windowHeight = options.windowHeight;

// If the caret position is below the view - keep it at the bottom edge.
// +---------------------------------------------+
// | editor viewport |
// | |
// | +--------------+ |
// | | | |
// | | view | |
// | | | |
// +-----+==============+------------------------+
// | |
// | █ - caret position |
// | |
// +---------------------------------------------+
if ( editorViewportRect.bottom < caretRect.bottom ) {
return Math.min( caretRect.top, editorViewportRect.bottom ) - viewHeight;
}

// If the view doesn't fit below the caret position and fits above, set it there.
// This means that the position below the caret is preferred.
// +---------------------------------------------+
// | |
// | editor viewport |
// | +--------------+ |
// | | | |
// | | view | |
// | | | |
// | +--------------+ |
// | █ - caret position |
// | |
// | |
// +---------------------------------------------+
// How much space is there for the view above and below the specified rect.
var spaceAbove = caretRect.top - editorViewportRect.top,
spaceBelow = editorViewportRect.bottom - caretRect.bottom,
viewExceedsTopViewport = ( caretRect.top - viewHeight ) < scrollPositionY;

if ( viewHeight > spaceBelow && viewHeight < spaceAbove && !viewExceedsTopViewport ) {
return caretRect.top - viewHeight;
}

// If the caret position is below the view - keep it at the bottom edge.
// +---------------------------------------------+
// | editor viewport |
// | |
// | +--------------+ |
// | | | |
// | | view | |
// | | | |
// +-----+==============+------------------------+
// | |
// | █ - caret position |
// | |
// +---------------------------------------------+
if ( editorViewportRect.bottom < rect.bottom ) {
top = Math.min( rect.top - viewHeight, editorViewportRect.bottom - viewHeight );
// If the caret position is above the view - keep it at the top edge.
// +---------------------------------------------+
// | |
// | █ - caret position |
// | |
// +-----+==============+------------------------+
// | | | |
// | | view | |
// | | | |
// | +--------------+ |
// | |
// | editor viewport |
// +---------------------------------------------+
if ( editorViewportRect.top > caretRect.top ) {
return Math.max( caretRect.bottom, editorViewportRect.top );
}

// (#3582)
// If the view goes beyond bottom window border - reverse view position, even if it fits editor viewport.
// +---------------------------------------------+
// | editor viewport |
// | |
// | |
// | +--------------+ |
// | | view | |
// | +--------------+ |
// | caret position - █ |
// | |
// =============================================== - bottom window border
// | |
// | |
// +---------------------------------------------+
var viewExceedsBottomViewport = ( caretRect.bottom + viewHeight ) > ( windowHeight + scrollPositionY );

if ( !( viewHeight > spaceBelow && viewHeight < spaceAbove ) && viewExceedsBottomViewport ) {
return caretRect.top - viewHeight;
}

// As a default, keep the view inside the editor viewport.
// +---------------------------------------------+
// | editor viewport |
// | |
// | |
// | |
// | █ - caret position |
// | +--------------+ |
// | | view | |
// | +--------------+ |
// | |
// | |
// +---------------------------------------------+
return Math.min( editorViewportRect.bottom, caretRect.bottom );
}

// If the caret position is above the view - keep it at the top edge.
// +---------------------------------------------+
// | |
// | █ - caret position |
// | |
// +-----+==============+------------------------+
// | | | |
// | | view | |
// | | | |
// | +--------------+ |
// | |
// | editor viewport |
// +---------------------------------------------+
if ( editorViewportRect.top > rect.top ) {
top = Math.max( rect.bottom, editorViewportRect.top );
function getHorizontalPosition( options ) {
var caretLeftPosition = options.leftPosition,
viewWidth = options.viewWidth,
windowWidth = options.windowWidth;

// (#3582)
// If the view goes beyond right window border - stick it to the edge of the available viewport.
// +---------------------------------------------+ ||
// | editor viewport | ||
// | | ||
// | | ||
// | caret position - █ | || - right window border
// | +--------------+||
// | | |||
// | | view |||
// | | |||
// | +--------------+||
// | | ||
// +---------------------------------------------+ ||
if ( caretLeftPosition + viewWidth > windowWidth ) {
return windowWidth - viewWidth;
}

// Otherwise inherit the horizontal position from caret.
return caretLeftPosition;
}

this.element.setStyles( {
left: rect.left + 'px',
top: top + 'px'
} );
// Bounding rect where the view should fit (visible editor viewport).
function getEditorViewportRect( editor ) {
var editable = editor.editable();

// iOS classic editor has different viewport element (#1910).
if ( CKEDITOR.env.iOS && !editable.isInline() ) {
return iOSViewportElement( editor ).getClientRect( true );
} else {
return editable.isInline() ? editable.getClientRect( true ) : editor.window.getFrame().getClientRect( true );
}
}
},

/**
Expand Down
2 changes: 1 addition & 1 deletion tests/plugins/autocomplete/autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@
// | | view | |
// | | | |
// | +--------------+ |
// | |
// | |
// | editor viewport |
// +---------------------------------------------+
viewport.fire( 'scroll' );
Expand Down
111 changes: 111 additions & 0 deletions tests/plugins/autocomplete/integration/emoji/manual/emoji.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<style>
body {
margin: 0;
padding-left: 350px;
position: static;
}

.wrapper {
position: absolute;
}

.wrapper.horizontal > div:not(:last-of-type) {
margin-right: 15px;
}

.wrapper.vertical > div:not(:last-of-type) {
margin-bottom: 15px;
}

.wrapper.horizontal {
display: inline-flex;
}

.wrapper.vertical {
flex-direction: column;
}

.wrapper.left {
left: 0;
}

.wrapper.right {
right: 0;
}

.wrapper.bottom {
bottom: 0;
}

.inline {
width: 300px;
height: 194px;
}
</style>

<div>
<p>Select mode</p>
<button type="button" id="left-button">Left</button>
<button type="button" id="right-button">Right</button>
<button type="button" id="bottom-button">Bottom</button>

<button type="button" id="reset-button">Reset</button>
</div>

<div class="wrapper" id="wrapper">
<div id="classic">
<h3>Classic editor</h3>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Expedita, veritatis. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Expedita, veritatis.</p>
</div>

<div id="divarea">
<h3>Divarea editor</h3>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Expedita, veritatis. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Expedita, veritatis.</p>
</div>

<div id="inline" class="inline" contenteditable="true">
<p style="padding-top: 40px;">Fake toolbar</p>
<h3>Inline editor</h3>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Expedita, veritatis. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Expedita, veritatis.</p>
</div>
</div>

<script>
bender.tools.ignoreUnsupportedEnvironment( 'emoji' );

if ( bender.tools.env.mobile ) {
bender.ignore();
}

var wrapper = CKEDITOR.document.getById( 'wrapper' ),
leftButton = CKEDITOR.document.getById( 'left-button' ),
rightButton = CKEDITOR.document.getById( 'right-button' ),
bottomButton = CKEDITOR.document.getById( 'bottom-button' ),
resetButton = CKEDITOR.document.getById( 'reset-button' );

leftButton.on( 'click', function() {
wrapper.setAttribute( 'class', 'wrapper vertical left' );
} );

rightButton.on( 'click', function() {
wrapper.setAttribute( 'class', 'wrapper vertical right' );
} );

bottomButton.on( 'click', function() {
wrapper.setAttribute( 'class', 'wrapper horizontal bottom' );
} );

resetButton.on( 'click', function() {
wrapper.setAttribute( 'class', 'wrapper' );
} );

var config = {
width: 300,
height: 180,
language: 'en'
};

CKEDITOR.replace( 'classic', config );
CKEDITOR.replace( 'divarea', CKEDITOR.tools.extend( { extraPlugins: 'divarea' }, config ) );
CKEDITOR.inline( 'inline', config );
</script>
17 changes: 17 additions & 0 deletions tests/plugins/autocomplete/integration/emoji/manual/emoji.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@bender-tags: 4.16.0, feature, 3582
@bender-ckeditor-plugins: wysiwygarea, toolbar, elementspath, sourcearea, emoji, clipboard, undo
@bender-ui: collapsed

1. Select mode (Left / Right / Bottom).
1. Place selection inside editor content as close as possible to window border.
1. Type `:da`.

### Expected:

Emoji panel should appear inside browser viewport and be fully visible.

### Unexpected:

Emoji panel appears outside browser borders and is not fully visible.

1. Play around with different caret placements to check if the panel is always visible and accessible.
Loading