2 * This file originates from https://github.com/ProseMirror/prosemirror-menu
3 * and is hence subject to the MIT license found here:
4 * https://github.com/ProseMirror/prosemirror-menu/blob/master/LICENSE
5 * @copyright Marijn Haverbeke and others
8 import crel from "crelt"
9 import {Plugin} from "prosemirror-state"
11 import {renderGrouped} from "./menu"
13 const prefix = "ProseMirror-menubar"
16 if (typeof navigator == "undefined") return false
17 let agent = navigator.userAgent
18 return !/Edge\/\d/.test(agent) && /AppleWebKit/.test(agent) && /Mobile\/\w+/.test(agent)
21 // :: (Object) → Plugin
22 // A plugin that will place a menu bar above the editor. Note that
23 // this involves wrapping the editor in an additional `<div>`.
26 // Supports the following options:
28 // content:: [[MenuElement]]
29 // Provides the content of the menu, as a nested array to be
30 // passed to `renderGrouped`.
33 // Determines whether the menu floats, i.e. whether it sticks to
34 // the top of the viewport when the editor is partially scrolled
36 export function menuBar(options) {
38 view(editorView) { return new MenuBarView(editorView, options) }
43 constructor(editorView, options) {
44 this.editorView = editorView
45 this.options = options
47 this.wrapper = crel("div", {class: prefix + "-wrapper"})
48 this.menu = this.wrapper.appendChild(crel("div", {class: prefix}))
49 this.menu.className = prefix
52 if (editorView.dom.parentNode)
53 editorView.dom.parentNode.replaceChild(this.wrapper, editorView.dom)
54 this.wrapper.appendChild(editorView.dom)
57 this.widthForMaxHeight = 0
60 let {dom, update} = renderGrouped(this.editorView, this.options.content)
61 this.contentUpdate = update
62 this.menu.appendChild(dom)
65 if (options.floating && !isIOS()) {
67 let potentialScrollers = getAllWrapping(this.wrapper)
68 this.scrollFunc = (e) => {
69 let root = this.editorView.root
70 if (!(root.body || root).contains(this.wrapper)) {
71 potentialScrollers.forEach(el => el.removeEventListener("scroll", this.scrollFunc))
73 this.updateFloat(e.target.getBoundingClientRect && e.target)
76 potentialScrollers.forEach(el => el.addEventListener('scroll', this.scrollFunc))
81 this.contentUpdate(this.editorView.state)
84 this.updateScrollCursor()
86 if (this.menu.offsetWidth != this.widthForMaxHeight) {
87 this.widthForMaxHeight = this.menu.offsetWidth
90 if (this.menu.offsetHeight > this.maxHeight) {
91 this.maxHeight = this.menu.offsetHeight
92 this.menu.style.minHeight = this.maxHeight + "px"
97 updateScrollCursor() {
98 let selection = this.editorView.root.getSelection()
99 if (!selection.focusNode) return
100 let rects = selection.getRangeAt(0).getClientRects()
101 let selRect = rects[selectionIsInverted(selection) ? 0 : rects.length - 1]
103 let menuRect = this.menu.getBoundingClientRect()
104 if (selRect.top < menuRect.bottom && selRect.bottom > menuRect.top) {
105 let scrollable = findWrappingScrollable(this.wrapper)
106 if (scrollable) scrollable.scrollTop -= (menuRect.bottom - selRect.top)
110 updateFloat(scrollAncestor) {
111 let parent = this.wrapper, editorRect = parent.getBoundingClientRect(),
112 top = scrollAncestor ? Math.max(0, scrollAncestor.getBoundingClientRect().top) : 0
115 if (editorRect.top >= top || editorRect.bottom < this.menu.offsetHeight + 10) {
116 this.floating = false
117 this.menu.style.position = this.menu.style.left = this.menu.style.top = this.menu.style.width = ""
118 this.menu.style.display = ""
119 this.spacer.parentNode.removeChild(this.spacer)
122 let border = (parent.offsetWidth - parent.clientWidth) / 2
123 this.menu.style.left = (editorRect.left + border) + "px"
124 this.menu.style.display = (editorRect.top > window.innerHeight ? "none" : "")
125 if (scrollAncestor) this.menu.style.top = top + "px"
128 if (editorRect.top < top && editorRect.bottom >= this.menu.offsetHeight + 10) {
130 let menuRect = this.menu.getBoundingClientRect()
131 this.menu.style.left = menuRect.left + "px"
132 this.menu.style.width = menuRect.width + "px"
133 if (scrollAncestor) this.menu.style.top = top + "px"
134 this.menu.style.position = "fixed"
135 this.spacer = crel("div", {class: prefix + "-spacer", style: `height: ${menuRect.height}px`})
136 parent.insertBefore(this.spacer, this.menu)
142 if (this.wrapper.parentNode)
143 this.wrapper.parentNode.replaceChild(this.editorView.dom, this.wrapper)
147 // Not precise, but close enough
148 function selectionIsInverted(selection) {
149 if (selection.anchorNode == selection.focusNode) return selection.anchorOffset > selection.focusOffset
150 return selection.anchorNode.compareDocumentPosition(selection.focusNode) == Node.DOCUMENT_POSITION_FOLLOWING
153 function findWrappingScrollable(node) {
154 for (let cur = node.parentNode; cur; cur = cur.parentNode)
155 if (cur.scrollHeight > cur.clientHeight) return cur
158 function getAllWrapping(node) {
160 for (let cur = node.parentNode; cur; cur = cur.parentNode)