jspanel4
Version:
A JavaScript library to create highly configurable multifunctional floating panels that can also be used as modal, tooltip, hint or contextmenu
330 lines (287 loc) • 14.7 kB
JavaScript
/**
* jsPanel - A JavaScript library to create highly configurable multifunctional floating panels that can also be used as modal, tooltip, hint or contextmenu
* @version v4.16.1
* @homepage https://jspanel.de/
* @license MIT
* @author Stefan Sträßer - info@jspanel.de
* @author of dialog extension: Michael Daumling - michael@terrapinlogo.com
* @github https://github.com/Flyer53/jsPanel4.git
*/
import {jsPanel} from '../../jspanel.js';
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent#Polyfill - needed for IE11
(function (window) {
try {
new MouseEvent('test');
return false; // No need to polyfill
} catch (e) {
// Need to polyfill - fall through
}
// Polyfills DOM4 MouseEvent
var MouseEvent = function (eventType, params) {
params = params || { bubbles: false, cancelable: false };
var mouseEvent = document.createEvent('MouseEvent');
mouseEvent.initMouseEvent(eventType, params.bubbles, params.cancelable, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
return mouseEvent;
};
MouseEvent.prototype = Event.prototype;
window.MouseEvent = MouseEvent;
})(window);
// -----------------------------------------------------------
if (!jsPanel.tooltip) {
jsPanel.tooltip = {
version: '1.4.0',
date: '2021-03-13 11:20',
defaults: {
//tip.options.position: is set in jsPanel.tooltip.create()
border: '1px',
dragit: false,
resizeit: false,
headerControls: 'none',
delay: 400,
ttipEvent: 'mouseenter',
ttipName: 'default',
parentPanelFront: false
},
create(options = {}, callback) {
options.paneltype = 'tooltip';
if (!options.id) {
options.id = `jsPanel-${jsPanel.idCounter += 1}`;
} else if (typeof options.id === 'function') {
options.id = options.id();
}
let target = options.target,
mode = options.mode || 'default',
timer;
if (typeof target === 'string') {
target = document.querySelector(target);
}
if (!target) {
try {throw new jsPanel.jsPanelError('TOOLTIP SETUP FAILED!<br>Either option target is missing in the tooltip configuration or the target does nor exist in the document!');}
catch (e) {jsPanel.error(e);}
return false;
}
// don't close tooltip or contextmenu on mousedown in target
jsPanel.pointerdown.forEach(function (evt) {
target.addEventListener(evt, e => {
e.stopPropagation();
}, false);
});
let opts = options;
if (options.config) {
opts = Object.assign({}, options.config, options);
delete opts.config;
}
opts = Object.assign({}, jsPanel.tooltip.defaults, opts);
opts.position = Object.assign({}, options.position);
opts.position.of = options.position.of || target;
target[opts.ttipName] = () => {
timer = window.setTimeout(function () {
// do nothing if id already exists in document
if (document.getElementById(options.id)) {
return false;
}
jsPanel.create(opts, function (panel) {
const tipToClose = panel,
closeTip = () => {
tipToClose.close();
target.removeEventListener('mouseleave', closeTip);
panel.removeEventListener('mouseleave', closeTip);
};
// by default tooltip will close when mouse leaves trigger
if (mode === 'default') {
target.addEventListener('mouseleave', closeTip, false);
} else if (mode === 'semisticky') {
// close tooltip when mouse leaves tooltip
panel.addEventListener('mouseleave', closeTip, false);
}
// some more tooltip specifics
panel.classList.add('jsPanel-tooltip');
panel.style.overflow = 'visible';
panel.header.style.cursor = 'default';
panel.footer.style.cursor = 'default';
// check whether tooltip is triggered from within a modal panel or panel and if so update z-index
if (target.closest('.jsPanel-modal')) {
panel.style.zIndex = target.closest('.jsPanel-modal').style.zIndex;
} else {
if (target.closest('.jsPanel') && opts.parentPanelFront) {
target.closest('.jsPanel').front();
}
panel.style.zIndex = jsPanel.zi.next();
}
// do not use 'click' instead of jsPanel.pointerdown
jsPanel.pointerdown.forEach(function (evt) {
panel.addEventListener(evt, (e) => {
e.stopPropagation();
}, false);
});
// add tooltip connector
if (opts.connector) {
let tipPos = jsPanel.tooltip.relativeTipPos(panel.options.position);
if (tipPos !== 'over') {
panel.append(jsPanel.tooltip.addConnector(panel, tipPos));
}
}
if (callback) {callback.call(panel, panel);}
});
}, opts.delay);
};
target.addEventListener(opts.ttipEvent, target[opts.ttipName], false);
// immediately show tooltip
if (opts.autoshow) {
let event = new MouseEvent('mouseenter');
target.dispatchEvent(event);
}
// do not create tooltip if mouse leaves target before delay elapsed
target.addEventListener('mouseleave', () => {
window.clearTimeout(timer);
}, false);
},
relativeTipPos(position) {
// returns the basic tip.options.position of the tooltip relative to option.tip.options.position.of (top, right, right-bottom etc.)
let relPos;
// TODO: relative positions leave out a few positions
if (position.my === 'center-bottom' && position.at === 'center-top') {
relPos = 'top';
} else if (position.my === 'left-center' && position.at === 'right-center') {
relPos = 'right';
} else if (position.my === 'center-top' && position.at === 'center-bottom') {
relPos = 'bottom';
} else if (position.my === 'right-center' && position.at === 'left-center') {
relPos = 'left';
} else if (position.my === 'right-bottom' && position.at === 'left-top') {
relPos = 'left-top-corner';
} else if (position.my === 'left-bottom' && position.at === 'right-right') {
relPos = 'right-top-corner';
} else if (position.my === 'left-top' && position.at === 'right-bottom') {
relPos = 'right-bottom-corner';
} else if (position.my === 'right-top' && position.at === 'left-bottom') {
relPos = 'left-bottom-corner';
} else if (position.my === 'left-bottom' && position.at === 'left-top') {
relPos = 'topleft';
} else if (position.my === 'right-bottom' && position.at === 'right-top') {
relPos = 'topright';
} else if (position.my === 'left-top' && position.at === 'right-top') {
relPos = 'righttop';
} else if (position.my === 'left-bottom' && position.at === 'right-bottom') {
relPos = 'rightbottom';
} else if (position.my === 'right-top' && position.at === 'right-bottom') {
relPos = 'bottomright';
} else if (position.my === 'left-top' && position.at === 'left-bottom') {
relPos = 'bottomleft';
} else if (position.my === 'right-bottom' && position.at === 'left-bottom') {
relPos = 'leftbottom';
} else if (position.my === 'right-top' && position.at === 'left-top') {
relPos = 'lefttop';
} else {relPos = 'over';}
return relPos;
},
addConnector(tip, relposition) {
let left, top, connCSS, connBg,
conn = document.createElement('div');
conn.className = `jsPanel-connector jsPanel-connector-${relposition}`;
// rest of tooltip positioning is in jspanel.sass
if (relposition === 'top' || relposition === 'topleft' || relposition === 'topright') {
tip.style.top = `calc(${tip.style.top} - 12px)`;
} else if (relposition === 'right' || relposition === 'righttop' || relposition === 'rightbottom') {
tip.style.left = `calc(${tip.style.left} + 12px)`;
} else if (relposition === 'bottom' || relposition === 'bottomleft' || relposition === 'bottomright') {
tip.style.top = `calc(${tip.style.top} + 12px)`;
} else if (relposition === 'left' || relposition === 'lefttop' || relposition === 'leftbottom') {
tip.style.left = `calc(${tip.style.left} - 12px)`;
}
if (typeof tip.options.connector === 'string') {
connBg = tip.options.connector;
} else {
connBg = window.getComputedStyle(tip).borderBottomColor;
}
if (relposition.match(/-/)) {
connCSS = {
left: left,
top: top,
backgroundColor: connBg
};
} else {
let styles = window.getComputedStyle(tip);
if (relposition === 'topleft' || relposition === 'topright') {
if (relposition === 'topleft') {
left = styles.borderBottomLeftRadius;
} else {
let corr = (24 + parseInt(styles.borderBottomLeftRadius)) + 'px';
left = `calc(100% - ${corr})`;
}
relposition = 'top';
} else if (relposition === 'bottomleft' || relposition === 'bottomright') {
if (relposition === 'bottomleft') {
left = styles.borderTopLeftRadius;
} else {
let corr = (24 + parseInt(styles.borderTopRightRadius)) + 'px';
left = `calc(100% - ${corr})`;
}
relposition = 'bottom';
} else if (relposition === 'lefttop' || relposition === 'leftbottom') {
if (relposition === 'lefttop') {
top = styles.borderTopRightRadius;
} else {
let corr = (24 + parseInt(styles.borderBottomRightRadius)) + 'px';
top = `calc(100% - ${corr})`;
}
relposition = 'left';
} else if (relposition === 'righttop' || relposition === 'rightbottom') {
if (relposition === 'righttop') {
top = styles.borderTopLeftRadius;
} else {
let corr = (24 + parseInt(styles.borderBottomLeftRadius)) + 'px';
top = `calc(100% - ${corr})`;
}
relposition = 'right';
}
connCSS = {
left: left,
top: top,
[`border-${relposition}-color`]: connBg
};
}
jsPanel.setStyle(conn, connCSS);
return conn;
},
// reposition is still experimental
reposition(tip, newposition, cb) {
setTimeout(function () { // switch of connector doesn't work properly without timeout
// newposition must be an object
// get option.tip.position.of
newposition.of = tip.options.position.of;
// reposition tooltip
tip.reposition(newposition);
// ... and add connector again
if (tip.options.connector) {
let connector = tip.querySelector('div.jsPanel-connector');
tip.removeChild(connector);
let tipPos = jsPanel.tooltip.relativeTipPos(newposition);
if (tipPos !== 'over') {
// relative positions leave out a few positions -> connectors are not added for some positions
tip.append(jsPanel.tooltip.addConnector(tip, tipPos));
}
}
if (cb) {cb.call(tip, tip);}
return tip;
},200);
},
// removes specific tooltip from target
remove(tgt, evt = 'mouseenter', tip = 'default') {
let tooltipToRemove = tgt[tip];
tgt.removeEventListener(evt, tooltipToRemove);
}
};
// close tooltips on pointerdown in document
jsPanel.pointerdown.forEach(function (evt) {
document.addEventListener(evt, function (e) {
document.querySelectorAll('.jsPanel-tooltip').forEach(function (item) {
if (!e.target.closest('.jsPanel-tooltip')) {
if (!item.options.autoshow) {
item.close();
}
}
});
},false);
});
}