blob: 9446f73a55ca6046e0531c0041f96cb9c3626af3 [file] [log] [blame]
/*
* Copyright (c) 2016-2020, The Cytoscape Consortium.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the “Software”), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/* eslint-disable */
const defaults = require('./defaults');
const assign = require('./assign');
const {
removeEles, setStyles, createElement, getPixelRatio, getOffset,
} = require('./dom-util');
const cxtmenu = function (params) {
const options = assign({}, defaults, params);
const cy = this;
const container = cy.container();
let target;
const data = {
options,
handlers: [],
container: createElement({ class: 'cxtmenu' }),
};
const wrapper = data.container;
const parent = createElement();
const canvas = createElement({ tag: 'canvas' });
let commands = [];
const c2d = canvas.getContext('2d');
let r = 100; // defailt radius;
let containerSize = (r + options.activePadding) * 2;
let activeCommandI;
let offset;
container.insertBefore(wrapper, container.firstChild);
wrapper.appendChild(parent);
parent.appendChild(canvas);
setStyles(wrapper, {
position: 'absolute',
zIndex: options.zIndex,
userSelect: 'none',
pointerEvents: 'none', // prevent events on menu in modern browsers
});
// prevent events on menu in legacy browsers
['mousedown', 'mousemove', 'mouseup', 'contextmenu'].forEach((evt) => {
wrapper.addEventListener(evt, (e) => {
e.preventDefault();
return false;
});
});
setStyles(parent, {
display: 'none',
width: `${containerSize}px`,
height: `${containerSize}px`,
position: 'absolute',
zIndex: 1,
marginLeft: `${-options.activePadding}px`,
marginTop: `${-options.activePadding}px`,
userSelect: 'none',
});
canvas.width = containerSize;
canvas.height = containerSize;
function createMenuItems() {
removeEles('.cxtmenu-item', parent);
const dtheta = 2 * Math.PI / (commands.length);
let theta1 = Math.PI / 2;
let theta2 = theta1 + dtheta;
for (let i = 0; i < commands.length; i++) {
const command = commands[i];
const midtheta = (theta1 + theta2) / 2;
const rx1 = 0.66 * r * Math.cos(midtheta);
const ry1 = 0.66 * r * Math.sin(midtheta);
const item = createElement({ class: 'cxtmenu-item' });
setStyles(item, {
color: options.itemColor,
cursor: 'default',
display: 'table',
'text-align': 'center',
// background: 'red',
position: 'absolute',
'text-shadow': `-1px -1px 2px ${options.itemTextShadowColor}, 1px -1px 2px ${options.itemTextShadowColor}, -1px 1px 2px ${options.itemTextShadowColor}, 1px 1px 1px ${options.itemTextShadowColor}`,
left: '50%',
top: '50%',
'min-height': `${r * 0.66}px`,
width: `${r * 0.66}px`,
height: `${r * 0.66}px`,
marginLeft: `${rx1 - r * 0.33}px`,
marginTop: `${-ry1 - r * 0.33}px`,
});
const content = createElement({ class: 'cxtmenu-content' });
if (command.content instanceof HTMLElement) {
content.appendChild(command.content);
} else {
content.innerHTML = command.content;
}
setStyles(content, {
width: `${r * 0.66}px`,
height: `${r * 0.66}px`,
'vertical-align': 'middle',
display: 'table-cell',
});
setStyles(content, command.contentStyle || {});
if (command.disabled === true || command.enabled === false) {
content.setAttribute('class', 'cxtmenu-content cxtmenu-disabled');
}
parent.appendChild(item);
item.appendChild(content);
theta1 += dtheta;
theta2 += dtheta;
}
}
function queueDrawBg(radius, rspotlight) {
redrawQueue.drawBg = [radius, rspotlight];
}
function drawBg(radius, rspotlight) {
rspotlight = rspotlight !== undefined ? rspotlight : rs;
c2d.globalCompositeOperation = 'source-over';
c2d.clearRect(0, 0, containerSize, containerSize);
// draw background items
c2d.fillStyle = options.fillColor;
const dtheta = 2 * Math.PI / (commands.length);
let theta1 = Math.PI / 2;
let theta2 = theta1 + dtheta;
for (let index = 0; index < commands.length; index++) {
const command = commands[index];
if (command.fillColor) {
c2d.fillStyle = command.fillColor;
}
c2d.beginPath();
c2d.moveTo(radius + options.activePadding, radius + options.activePadding);
c2d.arc(radius + options.activePadding, radius + options.activePadding, radius, 2 * Math.PI - theta1, 2 * Math.PI - theta2, true);
c2d.closePath();
c2d.fill();
theta1 += dtheta;
theta2 += dtheta;
c2d.fillStyle = options.fillColor;
}
// draw separators between items
c2d.globalCompositeOperation = 'destination-out';
c2d.strokeStyle = 'white';
c2d.lineWidth = options.separatorWidth;
theta1 = Math.PI / 2;
theta2 = theta1 + dtheta;
for (let i = 0; i < commands.length; i++) {
const rx1 = radius * Math.cos(theta1);
const ry1 = radius * Math.sin(theta1);
c2d.beginPath();
c2d.moveTo(radius + options.activePadding, radius + options.activePadding);
c2d.lineTo(radius + options.activePadding + rx1, radius + options.activePadding - ry1);
c2d.closePath();
c2d.stroke();
theta1 += dtheta;
theta2 += dtheta;
}
c2d.fillStyle = 'white';
c2d.globalCompositeOperation = 'destination-out';
c2d.beginPath();
c2d.arc(radius + options.activePadding, radius + options.activePadding, rspotlight + options.spotlightPadding, 0, Math.PI * 2, true);
c2d.closePath();
c2d.fill();
c2d.globalCompositeOperation = 'source-over';
}
function queueDrawCommands(rx, ry, radius, theta) {
redrawQueue.drawCommands = [rx, ry, radius, theta];
}
function drawCommands(rx, ry, radius, theta) {
const dtheta = 2 * Math.PI / (commands.length);
let theta1 = Math.PI / 2;
let theta2 = theta1 + dtheta;
theta1 += dtheta * activeCommandI;
theta2 += dtheta * activeCommandI;
c2d.fillStyle = options.activeFillColor;
c2d.strokeStyle = 'black';
c2d.lineWidth = 1;
c2d.beginPath();
c2d.moveTo(r + options.activePadding, r + options.activePadding);
c2d.arc(r + options.activePadding, r + options.activePadding, r + options.activePadding, 2 * Math.PI - theta1, 2 * Math.PI - theta2, true);
c2d.closePath();
c2d.fill();
c2d.fillStyle = 'white';
c2d.globalCompositeOperation = 'destination-out';
const tx = radius + options.activePadding + rx / radius * (rs + options.spotlightPadding - options.indicatorSize / 4);
const ty = radius + options.activePadding + ry / radius * (rs + options.spotlightPadding - options.indicatorSize / 4);
const rot = Math.PI / 4 - theta;
c2d.translate(tx, ty);
c2d.rotate(rot);
// clear the indicator
c2d.beginPath();
c2d.fillRect(-options.indicatorSize / 2, -options.indicatorSize / 2, options.indicatorSize, options.indicatorSize);
c2d.closePath();
c2d.fill();
c2d.rotate(-rot);
c2d.translate(-tx, -ty);
// c2d.setTransform( 1, 0, 0, 1, 0, 0 );
// clear the spotlight
c2d.beginPath();
c2d.arc(r + options.activePadding, radius + options.activePadding, rs + options.spotlightPadding, 0, Math.PI * 2, true);
c2d.closePath();
c2d.fill();
c2d.globalCompositeOperation = 'source-over';
}
function updatePixelRatio() {
const pxr = getPixelRatio();
const w = containerSize;
const h = containerSize;
canvas.width = w * pxr;
canvas.height = h * pxr;
canvas.style.width = `${w}px`;
canvas.style.height = `${h}px`;
c2d.setTransform(1, 0, 0, 1, 0, 0);
c2d.scale(pxr, pxr);
}
let redrawing = true;
let redrawQueue = {};
const raf = (
window.requestAnimationFrame
|| window.webkitRequestAnimationFrame
|| window.mozRequestAnimationFrame
|| window.msRequestAnimationFrame
|| ((fn) => setTimeout(fn, 16))
);
const redraw = function () {
if (redrawQueue.drawBg) {
drawBg.apply(null, redrawQueue.drawBg);
}
if (redrawQueue.drawCommands) {
drawCommands.apply(null, redrawQueue.drawCommands);
}
redrawQueue = {};
if (redrawing) {
raf(redraw);
}
};
// kick off
updatePixelRatio();
redraw();
let ctrx; let ctry; let
rs;
const bindings = {
on(events, selector, fn) {
let _fn = fn;
if (selector === 'core') {
_fn = function (e) {
if (e.cyTarget === cy || e.target === cy) { // only if event target is directly core
return fn.apply(this, [e]);
}
};
}
data.handlers.push({
events,
selector,
fn: _fn,
});
if (selector === 'core') {
cy.on(events, _fn);
} else {
cy.on(events, selector, _fn);
}
return this;
},
};
function addEventListeners() {
let grabbable;
let inGesture = false;
let dragHandler;
let zoomEnabled;
let panEnabled;
let boxEnabled;
let gestureStartEvent;
const restoreZoom = function () {
if (zoomEnabled) {
cy.userZoomingEnabled(true);
}
};
const restoreGrab = function () {
if (grabbable) {
target.grabify();
}
};
const restorePan = function () {
if (panEnabled) {
cy.userPanningEnabled(true);
}
};
const restoreBoxSeln = function () {
if (boxEnabled) {
cy.boxSelectionEnabled(true);
}
};
const restoreGestures = function () {
restoreGrab();
restoreZoom();
restorePan();
restoreBoxSeln();
};
window.addEventListener('resize', updatePixelRatio);
bindings
.on('resize', () => {
updatePixelRatio();
})
.on(options.openMenuEvents, options.selector, function (e) {
target = this; // Remember which node the context menu is for
const ele = this;
const isCy = this === cy;
if (inGesture) {
parent.style.display = 'none';
inGesture = false;
restoreGestures();
}
if (typeof options.commands === 'function') {
const res = options.commands(target);
if (res.then) {
res.then((_commands) => {
commands = _commands;
openMenu();
});
} else {
commands = res;
openMenu();
}
} else {
commands = options.commands;
openMenu();
}
function openMenu() {
if (!commands || commands.length === 0) { return; }
zoomEnabled = cy.userZoomingEnabled();
cy.userZoomingEnabled(false);
panEnabled = cy.userPanningEnabled();
cy.userPanningEnabled(false);
boxEnabled = cy.boxSelectionEnabled();
cy.boxSelectionEnabled(false);
grabbable = target.grabbable && target.grabbable();
if (grabbable) {
target.ungrabify();
}
let rp; let rw; let
rh;
if (!isCy && ele.isNode() && !ele.isParent() && !options.atMouse) {
rp = ele.renderedPosition();
rw = ele.renderedOuterWidth();
rh = ele.renderedOuterHeight();
} else {
rp = e.renderedPosition || e.cyRenderedPosition;
rw = 1;
rh = 1;
}
offset = getOffset(container);
ctrx = rp.x;
ctry = rp.y;
r = rw / 2 + options.menuRadius(target);
containerSize = (r + options.activePadding) * 2;
updatePixelRatio();
setStyles(parent, {
width: `${containerSize}px`,
height: `${containerSize}px`,
display: 'block',
left: `${rp.x - r}px`,
top: `${rp.y - r}px`,
});
createMenuItems();
rs = Math.max(rw, rh) / 2;
rs = Math.max(rs, options.minSpotlightRadius);
rs = Math.min(rs, options.maxSpotlightRadius);
queueDrawBg(r);
activeCommandI = undefined;
inGesture = true;
gestureStartEvent = e;
}
})
.on('cxtdrag tapdrag', options.selector, dragHandler = function (e) {
if (!inGesture) { return; }
const origE = e.originalEvent;
const isTouch = origE.touches && origE.touches.length > 0;
const pageX = (isTouch ? origE.touches[0].pageX : origE.pageX) - window.pageXOffset;
const pageY = (isTouch ? origE.touches[0].pageY : origE.pageY) - window.pageYOffset;
activeCommandI = undefined;
// refresh offset, because of scroll or resizing..
offset = getOffset(container);
let dx = pageX - offset.left - ctrx;
const dy = pageY - offset.top - ctry;
if (dx === 0) { dx = 0.01; }
const d = Math.sqrt(dx * dx + dy * dy);
const cosTheta = (dy * dy - d * d - dx * dx) / (-2 * d * dx);
let theta = Math.acos(cosTheta);
let rw;
if (target && typeof target.isNode === 'function' && target.isNode() && !target.isParent() && !options.atMouse) {
rw = target.renderedOuterWidth();
} else {
rw = 1;
}
r = rw / 2 + options.menuRadius(target);
if (d < rs + options.spotlightPadding) {
queueDrawBg(r);
return;
}
queueDrawBg(r);
const rx = dx * r / d;
const ry = dy * r / d;
if (dy > 0) {
theta = Math.PI + Math.abs(theta - Math.PI);
}
const dtheta = 2 * Math.PI / (commands.length);
let theta1 = Math.PI / 2;
let theta2 = theta1 + dtheta;
const isMouseCircleIn = Math.pow(r, 2) > (Math.pow(ctrx - (pageX - offset.left), 2) + Math.pow(ctry - (pageY - offset.top), 2));
for (let i = 0; i < commands.length; i++) {
const command = commands[i];
let inThisCommand = theta1 <= theta && theta <= theta2
|| theta1 <= theta + 2 * Math.PI && theta + 2 * Math.PI <= theta2;
if (command.disabled === true || command.enabled === false) {
inThisCommand = false;
}
if (isMouseCircleIn && inThisCommand) {
activeCommandI = i;
break;
}
theta1 += dtheta;
theta2 += dtheta;
}
queueDrawCommands(rx, ry, r, theta);
})
.on('tapdrag', dragHandler)
.on('cxttapend tapend', () => {
parent.style.display = 'none';
if (activeCommandI !== undefined) {
const { select } = commands[activeCommandI];
if (select) {
select.apply(target, [target, gestureStartEvent]);
activeCommandI = undefined;
}
}
inGesture = false;
restoreGestures();
});
}
function removeEventListeners() {
const { handlers } = data;
for (let i = 0; i < handlers.length; i++) {
const h = handlers[i];
if (h.selector === 'core') {
cy.off(h.events, h.fn);
} else {
cy.off(h.events, h.selector, h.fn);
}
}
window.removeEventListener('resize', updatePixelRatio);
}
function destroyInstance() {
redrawing = false;
removeEventListeners();
wrapper.remove();
}
addEventListeners();
return {
destroy() {
destroyInstance();
},
};
};
module.exports = cxtmenu;