164 lines
5.6 KiB
JavaScript
164 lines
5.6 KiB
JavaScript
|
/**
|
||
|
* This file originates from https://github.com/ProseMirror/prosemirror-menu
|
||
|
* and is hence subject to the MIT license found here:
|
||
|
* https://github.com/ProseMirror/prosemirror-menu/blob/master/LICENSE
|
||
|
* @copyright Marijn Haverbeke and others
|
||
|
*/
|
||
|
|
||
|
import crel from "crelt"
|
||
|
import {Plugin} from "prosemirror-state"
|
||
|
|
||
|
import {renderGrouped} from "./menu"
|
||
|
|
||
|
const prefix = "ProseMirror-menubar"
|
||
|
|
||
|
function isIOS() {
|
||
|
if (typeof navigator == "undefined") return false
|
||
|
let agent = navigator.userAgent
|
||
|
return !/Edge\/\d/.test(agent) && /AppleWebKit/.test(agent) && /Mobile\/\w+/.test(agent)
|
||
|
}
|
||
|
|
||
|
// :: (Object) → Plugin
|
||
|
// A plugin that will place a menu bar above the editor. Note that
|
||
|
// this involves wrapping the editor in an additional `<div>`.
|
||
|
//
|
||
|
// options::-
|
||
|
// Supports the following options:
|
||
|
//
|
||
|
// content:: [[MenuElement]]
|
||
|
// Provides the content of the menu, as a nested array to be
|
||
|
// passed to `renderGrouped`.
|
||
|
//
|
||
|
// floating:: ?bool
|
||
|
// Determines whether the menu floats, i.e. whether it sticks to
|
||
|
// the top of the viewport when the editor is partially scrolled
|
||
|
// out of view.
|
||
|
export function menuBar(options) {
|
||
|
return new Plugin({
|
||
|
view(editorView) { return new MenuBarView(editorView, options) }
|
||
|
})
|
||
|
}
|
||
|
|
||
|
class MenuBarView {
|
||
|
constructor(editorView, options) {
|
||
|
this.editorView = editorView
|
||
|
this.options = options
|
||
|
|
||
|
this.wrapper = crel("div", {class: prefix + "-wrapper"})
|
||
|
this.menu = this.wrapper.appendChild(crel("div", {class: prefix}))
|
||
|
this.menu.className = prefix
|
||
|
this.spacer = null
|
||
|
|
||
|
if (editorView.dom.parentNode)
|
||
|
editorView.dom.parentNode.replaceChild(this.wrapper, editorView.dom)
|
||
|
this.wrapper.appendChild(editorView.dom)
|
||
|
|
||
|
this.maxHeight = 0
|
||
|
this.widthForMaxHeight = 0
|
||
|
this.floating = false
|
||
|
|
||
|
let {dom, update} = renderGrouped(this.editorView, this.options.content)
|
||
|
this.contentUpdate = update
|
||
|
this.menu.appendChild(dom)
|
||
|
this.update()
|
||
|
|
||
|
if (options.floating && !isIOS()) {
|
||
|
this.updateFloat()
|
||
|
let potentialScrollers = getAllWrapping(this.wrapper)
|
||
|
this.scrollFunc = (e) => {
|
||
|
let root = this.editorView.root
|
||
|
if (!(root.body || root).contains(this.wrapper)) {
|
||
|
potentialScrollers.forEach(el => el.removeEventListener("scroll", this.scrollFunc))
|
||
|
} else {
|
||
|
this.updateFloat(e.target.getBoundingClientRect && e.target)
|
||
|
}
|
||
|
}
|
||
|
potentialScrollers.forEach(el => el.addEventListener('scroll', this.scrollFunc))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
update() {
|
||
|
this.contentUpdate(this.editorView.state)
|
||
|
|
||
|
if (this.floating) {
|
||
|
this.updateScrollCursor()
|
||
|
} else {
|
||
|
if (this.menu.offsetWidth != this.widthForMaxHeight) {
|
||
|
this.widthForMaxHeight = this.menu.offsetWidth
|
||
|
this.maxHeight = 0
|
||
|
}
|
||
|
if (this.menu.offsetHeight > this.maxHeight) {
|
||
|
this.maxHeight = this.menu.offsetHeight
|
||
|
this.menu.style.minHeight = this.maxHeight + "px"
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
updateScrollCursor() {
|
||
|
let selection = this.editorView.root.getSelection()
|
||
|
if (!selection.focusNode) return
|
||
|
let rects = selection.getRangeAt(0).getClientRects()
|
||
|
let selRect = rects[selectionIsInverted(selection) ? 0 : rects.length - 1]
|
||
|
if (!selRect) return
|
||
|
let menuRect = this.menu.getBoundingClientRect()
|
||
|
if (selRect.top < menuRect.bottom && selRect.bottom > menuRect.top) {
|
||
|
let scrollable = findWrappingScrollable(this.wrapper)
|
||
|
if (scrollable) scrollable.scrollTop -= (menuRect.bottom - selRect.top)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
updateFloat(scrollAncestor) {
|
||
|
let parent = this.wrapper, editorRect = parent.getBoundingClientRect(),
|
||
|
top = scrollAncestor ? Math.max(0, scrollAncestor.getBoundingClientRect().top) : 0
|
||
|
|
||
|
if (this.floating) {
|
||
|
if (editorRect.top >= top || editorRect.bottom < this.menu.offsetHeight + 10) {
|
||
|
this.floating = false
|
||
|
this.menu.style.position = this.menu.style.left = this.menu.style.top = this.menu.style.width = ""
|
||
|
this.menu.style.display = ""
|
||
|
this.spacer.parentNode.removeChild(this.spacer)
|
||
|
this.spacer = null
|
||
|
} else {
|
||
|
let border = (parent.offsetWidth - parent.clientWidth) / 2
|
||
|
this.menu.style.left = (editorRect.left + border) + "px"
|
||
|
this.menu.style.display = (editorRect.top > window.innerHeight ? "none" : "")
|
||
|
if (scrollAncestor) this.menu.style.top = top + "px"
|
||
|
}
|
||
|
} else {
|
||
|
if (editorRect.top < top && editorRect.bottom >= this.menu.offsetHeight + 10) {
|
||
|
this.floating = true
|
||
|
let menuRect = this.menu.getBoundingClientRect()
|
||
|
this.menu.style.left = menuRect.left + "px"
|
||
|
this.menu.style.width = menuRect.width + "px"
|
||
|
if (scrollAncestor) this.menu.style.top = top + "px"
|
||
|
this.menu.style.position = "fixed"
|
||
|
this.spacer = crel("div", {class: prefix + "-spacer", style: `height: ${menuRect.height}px`})
|
||
|
parent.insertBefore(this.spacer, this.menu)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
destroy() {
|
||
|
if (this.wrapper.parentNode)
|
||
|
this.wrapper.parentNode.replaceChild(this.editorView.dom, this.wrapper)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Not precise, but close enough
|
||
|
function selectionIsInverted(selection) {
|
||
|
if (selection.anchorNode == selection.focusNode) return selection.anchorOffset > selection.focusOffset
|
||
|
return selection.anchorNode.compareDocumentPosition(selection.focusNode) == Node.DOCUMENT_POSITION_FOLLOWING
|
||
|
}
|
||
|
|
||
|
function findWrappingScrollable(node) {
|
||
|
for (let cur = node.parentNode; cur; cur = cur.parentNode)
|
||
|
if (cur.scrollHeight > cur.clientHeight) return cur
|
||
|
}
|
||
|
|
||
|
function getAllWrapping(node) {
|
||
|
let res = [window]
|
||
|
for (let cur = node.parentNode; cur; cur = cur.parentNode)
|
||
|
res.push(cur)
|
||
|
return res
|
||
|
}
|