Skip to content

Commit

Permalink
refactor: improve positioning floating menu with floating-ui
Browse files Browse the repository at this point in the history
  • Loading branch information
sibiraj-s committed Feb 20, 2022
1 parent 1f7c335 commit 82c1047
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 51 deletions.
20 changes: 4 additions & 16 deletions docs/floating-menu.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Floating Menu

The editor exposes a component which renders a minimal menu. Place it anywhere in the HTML. Technically it is just a wrapper which provides a wrapper and positions it relative to selection. You can render anything inside it as required.
Include the floating menu in the template.

```html
<ngx-editor [editor]="editor">
Expand All @@ -10,32 +10,18 @@ The editor exposes a component which renders a minimal menu. Place it anywhere i

OR

HTML

```html
<div class="editor">
<ngx-editor [editor]="editor"> </ngx-editor>
<ngx-editor-floating-menu [editor]="editor"></ngx-editor-floating-menu>
</div>
```

CSS

```css
.editor {
position: relative; // important
}
```

**Note:** Make sure the wrapping element has relative position. Not required if placed inside the editor

### Default Floating menu

<iframe src="https://stackblitz.com/edit/ngx-editor-floating-menu?embed=1&hideExplorer=1&view=preview" height="600"></iframe>

### Floating Menu with custom element.

<iframe src="https://stackblitz.com/edit/ngx-editor-floating-menu-custom?embed=1&hideExplorer=1&view=preview" height="600"></iframe>
### Floating Menu with custom content.

```html
<div class="editor">
Expand All @@ -45,3 +31,5 @@ CSS
</ngx-editor-floating-menu>
</div>
```

<iframe src="https://stackblitz.com/edit/ngx-editor-floating-menu-custom?embed=1&hideExplorer=1&view=preview" height="600"></iframe>
1 change: 1 addition & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
// themeColor: '#28a745',
themeColor: '#0366d6',
loadSidebar: true,
subMaxLevel: 2,
};
</script>
<!-- Docsify v4 -->
Expand Down
26 changes: 25 additions & 1 deletion docs/menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Menu is not part of the editor component. Include `ngx-editor-menu` in your HTML manually.

## Component props
### Component props

- **editor** - (`Required`) editor instance
- **toolbar** - (`Optional`)
Expand Down Expand Up @@ -145,3 +145,27 @@ export class CustomMenuComponent implements OnInit {
}
}
```

## Floating Menu

The editor exposes a component which by default renders a minimal menu. Place it anywhere in the HTML. Technically it is just a wrapper which provides a wrapper and positions it relative to selection. You can render anything inside it as required.

```html
<ngx-editor [editor]="editor">
<ngx-editor-floating-menu [editor]="editor"></ngx-editor-floating-menu>
</ngx-editor>
```

OR

```html
<div class="editor">
<ngx-editor [editor]="editor"> </ngx-editor>
<ngx-editor-floating-menu [editor]="editor"></ngx-editor-floating-menu>
</div>
```

### Component props

- **editor** - (`Required`) editor instance
- **autoPlace** - (`Optional`) positions automatically to the top or bottom based on the space available. `false` by default
32 changes: 32 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
"@angular/compiler-cli": "~13.2.0",
"@commitlint/cli": "^16.2.1",
"@commitlint/config-conventional": "^16.2.1",
"@floating-ui/core": "^0.4.0",
"@floating-ui/dom": "^0.2.0",
"@types/jasmine": "~3.10.0",
"@types/node": "^12.11.1",
"@types/prosemirror-commands": "^1.0.4",
Expand Down
2 changes: 2 additions & 0 deletions projects/ngx-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"rxjs": ">=7.4.0"
},
"dependencies": {
"@floating-ui/core": "^0.4.0",
"@floating-ui/dom": "^0.2.0",
"@types/prosemirror-commands": "^1.0.4",
"@types/prosemirror-history": "^1.0.3",
"@types/prosemirror-inputrules":"^1.0.4",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { NodeSelection } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import { asyncScheduler, fromEvent, Subscription } from 'rxjs';
import { throttleTime } from 'rxjs/operators';
import { VirtualElement } from '@floating-ui/core';
import { computePosition, detectOverflow, offset, autoPlacement } from '@floating-ui/dom';

import Editor from '../../../Editor';
import { TBItems } from '../../../types';
Expand Down Expand Up @@ -39,6 +41,7 @@ export class FloatingMenuComponent implements OnInit, OnDestroy {
}

@Input() editor: Editor;
@Input() autoPlace = false;

private posLeft = 0;
private posTop = 0;
Expand Down Expand Up @@ -110,68 +113,125 @@ export class FloatingMenuComponent implements OnInit, OnDestroy {
this.showMenu = true;
}

private calculateBubblePosition(view: EditorView): BubblePosition {
private async calculateBubblePosition(view: EditorView): Promise<BubblePosition> {
const { state: { selection } } = view;
const { from } = selection;

// the floating bubble itself
const bubbleEl = this.el.nativeElement;
const bubble = bubbleEl.getBoundingClientRect();

// The box in which the tooltip is positioned, to use as base
const box = bubbleEl.parentElement.getBoundingClientRect();
const { from, to } = selection;

const start = view.coordsAtPos(from);
const end = view.coordsAtPos(to);

const selectionElement: VirtualElement = {
getBoundingClientRect() {
if (selection instanceof NodeSelection) {
const node = view.nodeDOM(from) as HTMLElement
return node.getBoundingClientRect()
}

const top = start.top
const bottom = end.bottom
const left = start.left
const right = end.right

return {
x: left,
y: top,
top,
bottom,
left,
right,
width: right - left,
height: bottom - top,
};
},
};

let left = start.left - box.left;

const overflowsRight = (
box.right < (start.left + bubble.width) ||
bubble.right > box.right
);

if (overflowsRight) {
left = box.width - bubble.width;
}

if (left < 0) {
left = 0;
}
// the floating bubble itself
const bubbleEl = this.el.nativeElement;

const bubbleHeight = bubble.height + parseInt(getComputedStyle(bubbleEl).marginBottom, 10);
const top = (start.top - box.top) - bubbleHeight;
const { x: left, y: top } = await computePosition(selectionElement, bubbleEl, {
placement: 'top',
middleware: [
offset(5),
this.autoPlace && autoPlacement({
boundary: view.dom,
padding: 5,
allowedPlacements: ['top', 'bottom']
}),
{
// prevent overflow on right and left side
// since only top and bottom placements are allowed
// autoplacement can't handle overflows on the right and left
name: 'overflowMiddleware',
async fn(middlewareArgs) {
const overflow = await detectOverflow(middlewareArgs, {
boundary: view.dom,
padding: 5,
});

// overflows left
if (overflow.left > 0) {
return {
x: middlewareArgs.x + overflow.left,
}
}

// overflows right
if (overflow.right > 0) {
return {
x: middlewareArgs.x - overflow.right,
}
}

return {};
},
}
].filter(Boolean)
})

return {
left,
top
};
}
}

private update(view: EditorView): void {
private canShowMenu(view: EditorView): Boolean {
const { state } = view;
const { selection } = state;
const { empty } = selection;

if (selection instanceof NodeSelection) {
if (selection.node.type.name === 'image') {
this.hide();
return;
return false;
}
}

const hasFocus = this.view.hasFocus();

if (!hasFocus || empty || this.dragging) {
this.hide();
return;
return false;
}

return true
}

private update(view: EditorView): void {
const canShowMenu = this.canShowMenu(view)

if (!canShowMenu) {
return this.hide()
}

const { top, left } = this.calculateBubblePosition(this.view);
this.calculateBubblePosition(this.view).then(({ top, left }) => {
if (!this.canShowMenu) {
return this.hide()
}

this.posLeft = left;
this.posTop = top;
this.posLeft = left;
this.posTop = top;

this.show();
this.show();
});
}

ngOnInit(): void {
Expand Down

0 comments on commit 82c1047

Please sign in to comment.