968 lines
32 KiB
JavaScript
968 lines
32 KiB
JavaScript
import { Elements } from "./utils.js"
|
|
import Poppable from "./poppable.js"
|
|
|
|
/** A Popup that shows text labels, images, or html
|
|
*/
|
|
export default class Popup extends Poppable {
|
|
/**
|
|
* Creates an instance of Popup.
|
|
* @param {any} [{
|
|
* parent = null, - The DOM parent element.
|
|
* content = null, - A dict object with type strings (text, img, html) as keys
|
|
* and corresponding values.
|
|
* context = window, - A context object for poppable elements
|
|
* fontSize = "1em", - Describes the font size as CSS value
|
|
* fontFamily = "Arial", - Describes the font family as CSS value
|
|
* padding = 16, - {number || string} padding - Describes the padding as CSS value
|
|
* notchSize = 10, - {number || string} notchSize - Describes the size of the notch (callout) as CSS value
|
|
* switchPos = false,
|
|
* minWidth = null,
|
|
* maxWidth = 800,
|
|
* backgroundColor = "#EEE", - The color of the background as CSS value
|
|
* normalColor = "#444", - normalColor - The color of textitems as CSS value
|
|
* notchPosition = "bottomLeft",
|
|
* zIndex = 0,
|
|
* keepWithin = null, - Ensure that popup is visible within the bounds of the given container
|
|
* autoClose = true, - Autoclose the Popup on tap
|
|
* closeIcon = null,
|
|
* resizeIcon = null,
|
|
* closeCommand = null,
|
|
* draggable = false
|
|
* noStyle = false - When true, prevents the popup from doing any aesthetic manipulations to the DOM leaving the styling completely to the style sheets.
|
|
* }={}]
|
|
* @memberof Popup
|
|
*/
|
|
constructor({
|
|
parent = null,
|
|
content = null,
|
|
context = window,
|
|
fontSize = "1em",
|
|
fontFamily = "Arial",
|
|
padding = 16,
|
|
notchSize = 10,
|
|
switchPos = false,
|
|
minWidth = null,
|
|
maxWidth = 800,
|
|
backgroundColor = "#EEE",
|
|
normalColor = "#444",
|
|
notchPosition = "bottomCenter",
|
|
zIndex = 0,
|
|
keepWithin = null,
|
|
autoClose = true,
|
|
closeIcon = null,
|
|
resizeIcon = null,
|
|
closeCommand = null,
|
|
draggable = false,
|
|
posOffset = 0,
|
|
targetBoundingBox = null,
|
|
useEventPosWithBoundingBox = false,
|
|
interactive = false,
|
|
onResize = null,
|
|
onMove = null,
|
|
noStyle = false,
|
|
hideOnUp = true,
|
|
} = {}) {
|
|
super()
|
|
this.context = context
|
|
this.noStyle = noStyle
|
|
this.hideOnUp = hideOnUp
|
|
this.padding = padding
|
|
this.notchPosition = notchPosition
|
|
this.notchSize = notchSize
|
|
this.switchPos = switchPos
|
|
this.fontSize = fontSize
|
|
this.fontFamily = fontFamily
|
|
this.minWidth = minWidth
|
|
this.maxWidth = maxWidth
|
|
this.normalColor = normalColor
|
|
this.backgroundColor = backgroundColor
|
|
this.keepWithin = keepWithin
|
|
this.autoClose = autoClose
|
|
this.resizeIcon = resizeIcon
|
|
this.closeIcon = closeIcon
|
|
this.closeCommand = closeCommand
|
|
this.zIndex = zIndex
|
|
this.parent = parent || document.body
|
|
this.draggable = draggable
|
|
this.posOffset = posOffset
|
|
this.targetBoundingBox = targetBoundingBox
|
|
this.useEventPosWithBoundingBox = useEventPosWithBoundingBox
|
|
this.currentPos = null
|
|
this.insertedNode = null
|
|
this.loaded = false
|
|
this.interactive = interactive
|
|
this.onload = null
|
|
this.onResize = onResize
|
|
this.onMove = onMove
|
|
if (content) {
|
|
this.show(content)
|
|
}
|
|
}
|
|
|
|
/** Setup popup with a dictionary of content types and contents.
|
|
* @param {Object} content - A dict object with type strings (text, img, html) as keys
|
|
* and corresponding values.
|
|
* @return {Popup} this
|
|
*/
|
|
setup(content) {
|
|
//console.log("Popup.setup", this.draggable)
|
|
this.content = content
|
|
this.items = {}
|
|
this.element = document.createElement("div")
|
|
this.element.classList.add("popup")
|
|
this.setAlpha(this.element, 0)
|
|
// this.element.style.opacity = 0
|
|
Elements.addClass(this.element, "unselectable")
|
|
this.notch = document.createElement("div")
|
|
Elements.setStyle(this.notch, this.notchStyle())
|
|
this.notch.className = "notch"
|
|
this.setupDraggable()
|
|
if (this.closeIcon) {
|
|
let img = document.createElement("img")
|
|
img.setAttribute("draggable", false)
|
|
img.src = this.closeIcon
|
|
img.style.position = "absolute"
|
|
img.style.right = "0px"
|
|
img.style.top = "0px"
|
|
img.style.width = "16px"
|
|
img.style.height = "16px"
|
|
img.onclick = e => {
|
|
this.close()
|
|
}
|
|
this.element.appendChild(img)
|
|
}
|
|
if (this.resizeIcon) {
|
|
let img = document.createElement("img")
|
|
img.style.position = "absolute"
|
|
img.style.right = "0px"
|
|
img.style.bottom = "0px"
|
|
img.style.width = "16px"
|
|
img.style.height = "16px"
|
|
img.src = this.resizeIcon
|
|
img.setAttribute("draggable", true)
|
|
img.ondragstart = e => {
|
|
this.currentPos = { x: e.clientX, y: e.clientY }
|
|
return true
|
|
}
|
|
img.ondrag = e => {
|
|
e.preventDefault()
|
|
|
|
let target = this.element.querySelector("iframe") || this.element
|
|
let delta = {
|
|
x: e.clientX - this.currentPos.x,
|
|
y: e.clientY - this.currentPos.y
|
|
}
|
|
|
|
this.currentPos = { x: e.clientX, y: e.clientY }
|
|
if (delta.x == 0 && delta.y == 0)
|
|
return
|
|
|
|
let rect = target.getBoundingClientRect()
|
|
let width = rect.width + delta.x
|
|
let height = rect.height + delta.y
|
|
target.style.width = width + "px"
|
|
target.style.height = height + "px"
|
|
|
|
switch (this.notchPosition) {
|
|
case "bottomLeft":
|
|
case "bottomCenter":
|
|
let bottom = parseFloat(this.element.style.bottom)
|
|
this.element.style.bottom = bottom - delta.y + "px"
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
//console.log("onResize", this.onResize)
|
|
if (this.onResize) {
|
|
this.onResize({ target, delta, width, height })
|
|
}
|
|
}
|
|
img.ondragend = e => { }
|
|
this.element.appendChild(img)
|
|
}
|
|
|
|
|
|
for (let key in content) {
|
|
switch (key) {
|
|
case "selector":
|
|
break
|
|
case "text":
|
|
let text = document.createElement("span")
|
|
this.element.appendChild(text)
|
|
text.innerHTML = content[key]
|
|
Elements.setStyle(text, { color: this.normalColor })
|
|
Elements.addClass(text, "unselectable")
|
|
Elements.addClass(text, "PopupContent")
|
|
this.insertedNode = text
|
|
this.loaded = true
|
|
break
|
|
case "img":
|
|
alert("img to be implemented")
|
|
break
|
|
case "iframe":
|
|
let iframe = document.createElement("iframe")
|
|
iframe.setAttribute("frameBorder", 0)
|
|
iframe.src = content[key]
|
|
iframe.onload = e => {
|
|
let body = iframe.contentWindow.document.body
|
|
let observer = new MutationObserver(() => {
|
|
this.iframeChanged(iframe)
|
|
})
|
|
observer.observe(iframe.contentWindow.document, {
|
|
attributes: true,
|
|
subtree: true,
|
|
childList: true,
|
|
characterData: true
|
|
})
|
|
let w = Math.max(body.scrollWidth, body.offsetWidth)
|
|
let h = Math.max(body.scrollHeight, body.offsetHeight)
|
|
iframe.style.width = w + "px"
|
|
iframe.style.height = h + "px"
|
|
this.layoutAfterInsert()
|
|
if (this.onload != null) {
|
|
this.onload()
|
|
}
|
|
this.loaded = true
|
|
}
|
|
this.element.appendChild(iframe)
|
|
Elements.addClass(iframe, "PopupContent")
|
|
this.insertIntoDOM()
|
|
return
|
|
case "html":
|
|
this.loaded = false
|
|
let div = document.createElement("div")
|
|
Elements.addClass(div, "PopupContent")
|
|
this.element.appendChild(div)
|
|
div.innerHTML = content.html
|
|
//console.log("insert", content)
|
|
let selector = content.selector
|
|
if (selector) {
|
|
this.insertedNode = div.querySelector(selector)
|
|
if (this.insertedNode == null) {
|
|
div.innerHTML = `<p style="color:red;">Popup content not found. Missing ${selector}</p>`
|
|
this.insertedNode = div.firstElementChild
|
|
}
|
|
}
|
|
else {
|
|
this.insertedNode = div.firstElementChild || div
|
|
}
|
|
this.setAlpha(this.insertedNode, 0)
|
|
let images = this.element.querySelectorAll('img')
|
|
let total = 0
|
|
if (images.length > 0) {
|
|
let count = 0
|
|
for (let image of images) {
|
|
if (!image.complete && !image.src.startsWith('data:')) {
|
|
total += 1
|
|
console.log("image not complete", image.src)
|
|
image.onload = e => {
|
|
count += 1
|
|
if (count == total) {
|
|
this.loaded = true
|
|
if (this.onload != null) {
|
|
this.onload()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (total == 0) {
|
|
this.loaded = true
|
|
}
|
|
break
|
|
case "node":
|
|
this.loaded = true
|
|
Elements.addClass(content.node, "PopupContent")
|
|
this.element.appendChild(content.node)
|
|
this.insertedNode = content.node
|
|
this.setAlpha(this.insertedNode, 0)
|
|
break
|
|
default:
|
|
alert("Unexpected content type: " + key)
|
|
break
|
|
}
|
|
}
|
|
this.insertIntoDOM()
|
|
this.layoutAfterInsert()
|
|
this.setupEventHandler()
|
|
return this
|
|
}
|
|
|
|
handleClose(e) {
|
|
let closing = this.closingEvent(e)
|
|
if (closing) {
|
|
this.close()
|
|
}
|
|
else {
|
|
this.setupCloseHandler()
|
|
}
|
|
}
|
|
|
|
setupCloseHandler() {
|
|
let close = this.handleClose
|
|
if (this.hideOnUp) {
|
|
if (window.PointerEvent)
|
|
this.parent.addEventListener("pointerup", close.bind(this), { capture: true, once: true })
|
|
else if (window.TouchEvent)
|
|
this.parent.addEventListener("touchend", close.bind(this), { capture: true, once: true })
|
|
else
|
|
this.parent.addEventListener("mouseup", close.bind(this), { capture: true, once: true })
|
|
} else {
|
|
if (window.PointerEvent)
|
|
this.parent.addEventListener("pointerdown", close.bind(this), { capture: true, once: true })
|
|
else if (window.TouchEvent)
|
|
this.parent.addEventListener("touchstart", close.bind(this), { capture: true, once: true })
|
|
else
|
|
this.parent.addEventListener("mousedown", close.bind(this), { capture: true, once: true })
|
|
}
|
|
}
|
|
|
|
setupEventHandler() {
|
|
if (this.autoClose) {
|
|
this.setupCloseHandler()
|
|
}
|
|
}
|
|
|
|
closingEvent(e) {
|
|
if (this.interactive) {
|
|
let node = e.target.closest(".PopupContent")
|
|
return node == null
|
|
}
|
|
return true
|
|
}
|
|
|
|
iframeChanged(iframe) {
|
|
let body = iframe.contentWindow.document.body
|
|
let w = Math.max(body.scrollWidth, body.offsetWidth)
|
|
let h = Math.max(body.scrollHeight, body.offsetHeight)
|
|
iframe.style.width = w + "px"
|
|
iframe.style.height = h + "px"
|
|
}
|
|
|
|
setupDraggable() {
|
|
if (this.draggable) {
|
|
let target = this.element
|
|
target.setAttribute("draggable", true)
|
|
target.ondragstart = e => {
|
|
this.currentPos = { x: e.clientX, y: e.clientY }
|
|
var img = document.createElement('img')
|
|
img.src = 'data:image/gifbase64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
|
|
e.dataTransfer.setDragImage(img, 0, 0)
|
|
}
|
|
target.ondrag = e => {
|
|
e.preventDefault()
|
|
let delta = {
|
|
x: e.clientX - this.currentPos.x,
|
|
y: e.clientY - this.currentPos.y
|
|
}
|
|
this.currentPos = { x: e.clientX, y: e.clientY }
|
|
let left = parseFloat(target.style.left)
|
|
let top = parseFloat(target.style.top)
|
|
|
|
target.style.left = left + delta.x + 'px'
|
|
target.style.top = top + delta.y + 'px'
|
|
|
|
//console.log("Popup.ondrag", target, event.target)
|
|
if (this.onMove) {
|
|
this.onMove({ target, delta })
|
|
}
|
|
|
|
this.lastDrag = { left, top }
|
|
}
|
|
target.ondragend = e => {
|
|
target.style.left = this.lastDrag.left + 'px'
|
|
target.style.top = this.lastDrag.top + 'px'
|
|
this.currentPos = null
|
|
}
|
|
}
|
|
}
|
|
|
|
moveDragged(target) {
|
|
|
|
}
|
|
|
|
insertIntoDOM(layout = true) {
|
|
this.setAlpha(this.insertedNode, 0)
|
|
this.element.appendChild(this.notch)
|
|
this.parent.appendChild(this.element)
|
|
}
|
|
|
|
layoutAfterInsert() {
|
|
Elements.setStyle(this.element, this.defaultStyle())
|
|
this.layout()
|
|
//this.element.style.opacity = 1
|
|
}
|
|
|
|
/** Layout the menu items. Needed only in the subclass.
|
|
*/
|
|
layout() { }
|
|
|
|
|
|
remove() {
|
|
if (this.parent.contains(this.element))
|
|
this.parent.removeChild(this.element)
|
|
this.unregister(this.context)
|
|
}
|
|
|
|
/** Close and remove the Popup from the DOM tree.
|
|
*/
|
|
close() {
|
|
//console.log("Popup.close", this.closeCommand)
|
|
this.unregister(this.context)
|
|
if (this.closeCommand) {
|
|
this.closeCommand(this, () => this.remove())
|
|
}
|
|
else {
|
|
this.remove()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the alpha value to show or hide the popup. Uses CSS transitions.
|
|
* (A former dependency on TweenLite has beeen removed.)
|
|
*
|
|
* @param {*} targets
|
|
* @param {*} value
|
|
* @memberof Popup
|
|
*/
|
|
setAlpha(targets, value) {
|
|
let objs = (targets instanceof Array) ? targets : [targets]
|
|
for (let obj of objs) {
|
|
if (value) {
|
|
obj.style.transition = "opacity 0.2s ease-in"
|
|
}
|
|
obj.style.opacity = value
|
|
}
|
|
// if (value) {
|
|
// TweenLite.to(targets, 0.2, { autoAlpha: value })
|
|
// }
|
|
// else {
|
|
// TweenLite.set(targets, { autoAlpha: 0 })
|
|
// }
|
|
}
|
|
|
|
/**
|
|
* Starts a fade in animation.
|
|
*
|
|
* @memberof Popup
|
|
*/
|
|
fadeIn() {
|
|
this.setAlpha([this.element, this.insertedNode], 1)
|
|
}
|
|
|
|
/** Shows the Popup with the given commands at the specified point.
|
|
* @param {Object} content - A dict object with type strings (text, img, html) as keys
|
|
* and corresponding values.
|
|
* @param {Point} point - The position as x, y coordinates {px}.
|
|
* @return {Popup} this
|
|
*/
|
|
showAt(content, point) {
|
|
this.setup(content)
|
|
console.log("showAt", this.loaded)
|
|
if (this.loaded) {
|
|
this.placeAt(point)
|
|
this.fadeIn()
|
|
}
|
|
else {
|
|
this.setAlpha([this.element, this.insertedNode], 0)
|
|
this.onload = () => {
|
|
this.layoutAfterInsert()
|
|
this.placeAt(point)
|
|
this.fadeIn()
|
|
}
|
|
}
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Place the origin, i.e. the upper left corner at the given position using CSS styles.
|
|
*
|
|
* @param {any} x
|
|
* @param {any} y
|
|
* @memberof Popup
|
|
*/
|
|
placeOrigin(x, y) {
|
|
Elements.setStyle(this.element, { left: x + "px", top: y + "px" })
|
|
}
|
|
|
|
/**
|
|
* Calculate the local coordinates within the keepWithin container.
|
|
*
|
|
* @param {any} x
|
|
* @param {any} y
|
|
* @returns
|
|
* @memberof Popup
|
|
*/
|
|
localPointWithin(x, y, width, height) {
|
|
let pt = { x, y }
|
|
return pt
|
|
}
|
|
|
|
withinDimensions() {
|
|
return {
|
|
width: this.keepWithin.offsetWidth,
|
|
height: this.keepWithin.offsetHeight
|
|
}
|
|
}
|
|
|
|
localDimensions() {
|
|
return {
|
|
width: this.element.offsetWidth,
|
|
height: this.element.offsetHeight
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the notch position depending on the local coordinates within the keepWithin container
|
|
* Divides the space vertically into top, center, bottom and horizontally into left, center, right
|
|
*
|
|
* @param {any} x
|
|
* @param {any} y
|
|
* @returns
|
|
* @memberof Popup
|
|
*/
|
|
notchPositionWithin(x, y) {
|
|
let horizontal = "Center"
|
|
let vertical = "center"
|
|
let { width, height } = this.withinDimensions()
|
|
let local = this.localPointWithin(x, y, width, height)
|
|
if (local.y < height * 0.33) {
|
|
vertical = "top"
|
|
}
|
|
if (local.y > height * 0.66) {
|
|
vertical = "bottom"
|
|
}
|
|
if (local.x < width * 0.33) {
|
|
horizontal = "Left"
|
|
}
|
|
if (local.x > width * 0.66) {
|
|
horizontal = "Right"
|
|
}
|
|
let result = vertical + horizontal
|
|
if (result == "centerCenter")
|
|
return this.notchPosition
|
|
return result
|
|
}
|
|
|
|
placeAt(point) {
|
|
let x = point.x
|
|
let y = point.y
|
|
let notchPosition = this.notchPosition
|
|
if (this.keepWithin != null) {
|
|
notchPosition = this.notchPositionWithin(x, y)
|
|
}
|
|
Elements.setStyle(this.notch, this.notchStyle(notchPosition))
|
|
this.notch.className = "notch " + notchPosition
|
|
let { width, height } = this.localDimensions()
|
|
|
|
//if targetBoundingBox is set, popup is placed next to the rectangle
|
|
if (this.targetBoundingBox) {
|
|
let bbTop = this.targetBoundingBox.y
|
|
let bbBottom = this.targetBoundingBox.y + this.targetBoundingBox.height
|
|
let bbLeft = this.targetBoundingBox.x
|
|
let bbRight = this.targetBoundingBox.x + this.targetBoundingBox.width
|
|
//console.log("place popup with bb set:", x, y, bbTop, bbBottom, bbLeft, bbRight)
|
|
switch (notchPosition) {
|
|
case "bottomLeft":
|
|
case "bottomRight":
|
|
case "bottomCenter":
|
|
y = bbTop
|
|
if (!this.useEventPosWithBoundingBox)
|
|
x = (bbLeft + bbRight) / 2
|
|
break
|
|
case "topLeft":
|
|
case "topRight":
|
|
case "topCenter":
|
|
y = bbBottom
|
|
if (!this.useEventPosWithBoundingBox)
|
|
x = (bbLeft + bbRight) / 2
|
|
break
|
|
case "centerRight":
|
|
x = bbLeft
|
|
if (!this.useEventPosWithBoundingBox)
|
|
y = (bbTop + bbBottom) / 2
|
|
break
|
|
case "centerLeft":
|
|
x = bbRight
|
|
if (!this.useEventPosWithBoundingBox)
|
|
y = (bbTop + bbBottom) / 2
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
//calculate position depending on several (optional) parameters
|
|
switch (notchPosition) {
|
|
case "bottomLeft":
|
|
x -= this.padding
|
|
x -= this.notchSize
|
|
y -= height
|
|
y -= this.notchSize * 2
|
|
y -= this.posOffset
|
|
break
|
|
case "bottomRight":
|
|
x -= width
|
|
x += this.padding
|
|
x += this.notchSize
|
|
y -= height
|
|
y -= this.notchSize * 2
|
|
y -= this.posOffset
|
|
break
|
|
case "bottomCenter":
|
|
x -= width / 2
|
|
y -= height
|
|
y -= this.notchSize * 2
|
|
y -= this.posOffset
|
|
break
|
|
case "topLeft":
|
|
x -= this.padding
|
|
x -= this.notchSize
|
|
y += this.notchSize * 2
|
|
y += this.posOffset
|
|
break
|
|
case "topRight":
|
|
x -= width
|
|
x += this.padding
|
|
x += this.notchSize
|
|
y += this.notchSize * 2
|
|
y += this.posOffset
|
|
break
|
|
case "topCenter":
|
|
x -= width / 2
|
|
y += this.notchSize * 2
|
|
y += this.posOffset
|
|
break
|
|
case "centerRight":
|
|
x -= width + this.notchSize * 2
|
|
x -= this.posOffset
|
|
y -= height / 2
|
|
break
|
|
case "centerLeft":
|
|
//console.log("height", height)
|
|
y -= height / 2
|
|
x += this.notchSize * 2
|
|
x += this.posOffset
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
this.placeOrigin(x, y)
|
|
}
|
|
|
|
/** Shows the Popup with the given commands at the current position.
|
|
* @param {Object} content - A dict object with type strings (text, img, html) as keys
|
|
* and corresponding values.
|
|
* @return {Popup} this
|
|
*/
|
|
show(content) {
|
|
this.setup(content)
|
|
this.fadeIn()
|
|
return this
|
|
}
|
|
|
|
/** Configuration object. Return default styles as CSS values.
|
|
*/
|
|
defaultStyle() {
|
|
let padding = this.padding
|
|
let style = {
|
|
maxWidth: this.maxWidth + "px",
|
|
zIndex: this.zIndex,
|
|
position: "absolute",
|
|
}
|
|
if (this.minWidth) {
|
|
style.minWidth = this.minWidth + "px"
|
|
}
|
|
if (!this.noStyle) {
|
|
Object.assign(style, {
|
|
borderRadius: Math.round(this.padding / 2) + "px",
|
|
backgroundColor: this.backgroundColor,
|
|
padding: this.padding + "px",
|
|
boxShadow: "0 10px 15px rgba(0, 0, 0, 0.3)",
|
|
fontFamily: this.fontFamily,
|
|
fontSize: this.fontSize,
|
|
stroke: "black",
|
|
fill: "white"
|
|
})
|
|
}
|
|
|
|
return style
|
|
}
|
|
|
|
/** Configuration object. Return notch styles as CSS values.
|
|
*/
|
|
notchStyle(notchPosition = null) {
|
|
if (notchPosition == null) {
|
|
notchPosition = this.notchPosition
|
|
}
|
|
let width = 0
|
|
let height = 0
|
|
let left = this.padding
|
|
let size = this.localDimensions()
|
|
if (notchPosition.endsWith("Right")) {
|
|
left = size.width - this.padding - this.notchSize * 2
|
|
}
|
|
if (notchPosition.endsWith("Center")) {
|
|
left = size.width / 2 - this.notchSize
|
|
}
|
|
left = Math.round(left) + 'px'
|
|
let borderBottom = 0
|
|
let borderTop = 0
|
|
|
|
if (notchPosition.startsWith("bottom")) {
|
|
if (this.noStyle) {
|
|
return {
|
|
width,
|
|
height,
|
|
left,
|
|
bottom: -this.notchSize + "px",
|
|
position: "absolute",
|
|
borderStyle: "solid",
|
|
borderTopWidth: this.notchSize + "px",
|
|
borderRight: this.notchSize + "px solid transparent",
|
|
borderLeft: this.notchSize + "px solid transparent",
|
|
borderBottom: 0
|
|
}
|
|
|
|
} else {
|
|
return {
|
|
width,
|
|
height,
|
|
left,
|
|
boxShadow: "0 12px 15px rgba(0, 0, 0, 0.1)",
|
|
bottom: -this.notchSize + "px",
|
|
position: "absolute",
|
|
borderTop: this.notchSize + "px solid " + this.backgroundColor,
|
|
borderRight: this.notchSize + "px solid transparent",
|
|
borderLeft: this.notchSize + "px solid transparent",
|
|
borderBottom: 0
|
|
}
|
|
}
|
|
}
|
|
if (notchPosition.startsWith("top")) {
|
|
if (this.noStyle) {
|
|
return {
|
|
width,
|
|
height,
|
|
left,
|
|
top: -this.notchSize + "px",
|
|
position: "absolute",
|
|
borderStyle: "solid",
|
|
borderBottomWidth: this.notchSize + "px",
|
|
borderRight: this.notchSize + "px solid transparent",
|
|
borderLeft: this.notchSize + "px solid transparent",
|
|
borderTop: 0
|
|
}
|
|
} else {
|
|
return {
|
|
width,
|
|
height,
|
|
left,
|
|
top: -this.notchSize + "px",
|
|
position: "absolute",
|
|
borderBottom: this.notchSize + "px solid " + this.backgroundColor,
|
|
borderRight: this.notchSize + "px solid transparent",
|
|
borderLeft: this.notchSize + "px solid transparent",
|
|
borderTop: 0
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.noStyle) {
|
|
|
|
if (notchPosition.endsWith("Left")) {
|
|
left = -this.notchSize * 2 + "px"
|
|
}
|
|
|
|
if (notchPosition.endsWith("Right")) {
|
|
left = size.width + "px"
|
|
}
|
|
|
|
|
|
let top = size.height / 2 - this.notchSize
|
|
top = Math.round(top) + 'px'
|
|
|
|
|
|
return {
|
|
width,
|
|
height,
|
|
left,
|
|
top,
|
|
borderRightWidth: this.notchSize,
|
|
borderLeftWidth: this.notchSize,
|
|
position: "absolute",
|
|
borderTop: this.notchSize + "px solid transparent",
|
|
borderBottom: this.notchSize + "px solid transparent"
|
|
}
|
|
|
|
} else {
|
|
let borderRight = this.notchSize + "px solid transparent"
|
|
let borderLeft = this.notchSize + "px solid transparent"
|
|
let top = size.height / 2 - this.notchSize
|
|
if (notchPosition.endsWith("Left")) {
|
|
left = -this.notchSize * 2 + "px"
|
|
borderRight = this.notchSize + "px solid " + this.backgroundColor
|
|
this.element.style.boxShadow = "15px 10px 15px rgba(0, 0, 0, 0.3)"
|
|
}
|
|
if (notchPosition.endsWith("Right")) {
|
|
left = size.width + "px"
|
|
borderLeft = this.notchSize + "px solid " + this.backgroundColor
|
|
this.element.style.boxShadow = "15px 5px 15px rgba(0, 0, 0, 0.3)"
|
|
}
|
|
|
|
top = Math.round(top) + 'px'
|
|
|
|
|
|
return {
|
|
width,
|
|
height,
|
|
left,
|
|
top,
|
|
borderRight,
|
|
borderLeft,
|
|
// boxShadow,
|
|
position: "absolute",
|
|
borderTop: this.notchSize + "px solid transparent",
|
|
borderBottom: this.notchSize + "px solid transparent"
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Convenient static methods to show and reuse a Popup implemented
|
|
* as a class variable.
|
|
* @param {Object} content - A dict object with type strings (text, img, html) as keys
|
|
* and corresponding values.
|
|
* @param {Point} point - The position as x, y coordinates {px}.
|
|
* @param {boolean} autoClose - Autoclose the menu after selecting an item.
|
|
*/
|
|
static open(
|
|
content,
|
|
point,
|
|
{
|
|
parent = null,
|
|
context = window,
|
|
fontSize = "1em",
|
|
fontFamily = "Arial",
|
|
padding = 16,
|
|
notchSize = 10,
|
|
switchPos = false,
|
|
minWidth = null,
|
|
maxWidth = 800,
|
|
backgroundColor = "#EEE",
|
|
zIndex = 0,
|
|
normalColor = "#444",
|
|
closeIcon = null,
|
|
resizeIcon = null,
|
|
closeCommand = null,
|
|
autoClose = true,
|
|
keepWithin = null,
|
|
draggable = false,
|
|
posOffset = 0,
|
|
targetBoundingBox = null,
|
|
useEventPosWithBoundingBox = false,
|
|
interactive = false,
|
|
onResize = null,
|
|
onMove = null
|
|
} = {}
|
|
) {
|
|
|
|
|
|
let notchPosition = (switchPos && point.y < 50) ? "topCenter" : "bottomCenter"
|
|
let popup = new Popup({
|
|
parent,
|
|
context,
|
|
fontFamily,
|
|
fontSize,
|
|
padding,
|
|
notchSize,
|
|
switchPos,
|
|
minWidth,
|
|
maxWidth,
|
|
backgroundColor,
|
|
normalColor,
|
|
notchPosition,
|
|
zIndex,
|
|
autoClose,
|
|
keepWithin,
|
|
closeCommand,
|
|
closeIcon,
|
|
resizeIcon,
|
|
draggable,
|
|
posOffset,
|
|
targetBoundingBox,
|
|
useEventPosWithBoundingBox,
|
|
interactive,
|
|
onResize,
|
|
onMove
|
|
})
|
|
popup.register(context)
|
|
popup.showAt(content, point)
|
|
return popup
|
|
}
|
|
|
|
/** Convenient static method to close the Popup implemented
|
|
* as a class variable. Calls the close command.
|
|
*/
|
|
static closePopup(context = window) {
|
|
let popup = Poppable.registrations.get(context)
|
|
if (popup != null) {
|
|
popup.close()
|
|
}
|
|
}
|
|
|
|
/** Convenient static methods to remove the Popup implemented
|
|
* as a class variable. Removes the popup without performing the close command.
|
|
*/
|
|
static remove(context = window) {
|
|
let popup = Poppable.registrations.get(context)
|
|
if (popup != null) {
|
|
popup.remove()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convenient static method to compute the clicked rect of objects that have multiple clients rects.
|
|
* Needed to position popups correctly above objects with line breaks, e.g. spans
|
|
*
|
|
* @static
|
|
* @param {*} event
|
|
* @returns {*} DOMRect
|
|
* @memberof Popup
|
|
*/
|
|
static targetRect(event) {
|
|
let target = event.target
|
|
let x = event.pageX
|
|
let y = event.pageY
|
|
for (let rect of target.getClientRects()) {
|
|
let withinX = x >= rect.left && x <= rect.left + rect.width
|
|
let withinY = y >= rect.top && y <= rect.top + rect.height
|
|
if (withinX && withinY) {
|
|
return rect
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Convenient static method to compute the center of objects that have multiple clients rects.
|
|
* Needed to position popups correctly above objects with line breaks, e.g. spans
|
|
*
|
|
* @static
|
|
* @param {*} event
|
|
* @returns {*} Point
|
|
* @memberof Popup
|
|
*/
|
|
static targetCenter(event) {
|
|
let target = event.target
|
|
let x = event.pageX
|
|
let y = event.pageY
|
|
let rect = Popup.targetRect(event)
|
|
if (rect != null) {
|
|
x = rect.left + rect.width / 2
|
|
y = rect.top + rect.height / 2
|
|
}
|
|
return { x, y }
|
|
}
|
|
}
|