It's for a combination of reasons:
- The need of a tooltip feature for my github page and my moduleEditor where I'm working on.
- Learning and get some experiences with the 'pointer api', to make such a feature.
A 'light weight', 'OOP' tooltipModule for use on module devices.
I kept it simple, just a 'rectangle' that pops up as a replacement of the 'title'. attribute.
- tooltipModule
- factory
- handlers
☛ pointer_events_handler.js 1
☛ pointer_evt_types_handler.js 2 - module_functions.js 3
- handlers
- tooltip.css 4
- tooltip_ctn.js 5
- tooltip_module.js 6
- tooltip_create.js 7
- tooltip_actions.js 8
- factory
- Using 'async iife''s in the 'class constructor' explained!
☛ What the 'class constructor' finally wants are objects!
☛ What an 'iife' finally produces is an object! - I never export classes directly!
- I pass them to an arrow function and that is what I export.
- Anytime I call those functions they are new, without using the keyword 'new' !
- If I want objects, I use 'obj_args', otherwise I use 'args'!
1: (pointerEventsHandler)
import * as MFT from './../mdl_functions.js';
class PointerEventsHandler{
#evt_type;
#evt_cb;
#parent_el;
constructor(...args){
const [parent_el,evt_type,evt_cb,evt_options = false] = args
this.#evt_type = evt_type;
this.#evt_cb = evt_cb;
this.#parent_el = parent_el ?? null;
(async()=> {
if(this.#parent_el !== null){
this.#parent_el.addEventListener(this.#evt_type,this.#evt_cb,evt_options);
}
})();
}
}
export const pointerEventsHandler = async (...args)=>{2: (pointerEvtTypesHandler)
import * as MFT from './../mdl_functions.js';
import {pointerEventsHandler} from './pointer_events_handler.js';
class PointerEvtTypesHandler{
#evt_type;
#evt_cb_mouse;
#evt_cb_pen;
#evt_cb_touch;
#parent_el;
evt_target;
constructor(obj_args){
const {parent_el,evt_type,evt_cb_mouse,evt_cb_pen,evt_cb_touch,
prevent_default = true,evt_options = false} = obj_args;
this.#evt_cb_mouse = evt_cb_mouse ?? null;
this.#evt_cb_pen = evt_cb_pen ?? null;
this.#evt_cb_touch = evt_cb_touch ?? null;
this.#evt_type = evt_type;
this.#parent_el = parent_el ?? null;
(async()=> {
if(this.#parent_el !== null){
const evt_manipulator= async(evt)=>{
if(prevent_default === true){
evt.preventDefault();
}
this.evt_target = evt.target;
switch(evt.pointerType){
case "mouse":{
if(this.#evt_cb_mouse !== null){
await this.#evt_cb_mouse(evt,this.evt_target);
}
}
break;
case "pen":{
if(this.#evt_cb_pen !== null){
await this.#evt_cb_pen(evt,this.evt_target);
}
}
break;
case "touch":{
if(this.#evt_cb_touch !== null){
await this.#evt_cb_touch(evt,this.evt_target);
}
}
break;
default:{
console.log(`pointerType ${evt.pointerType} is not supported`);
}
}
}
await pointerEventsHandler(this.#parent_el,this.#evt_type,evt_manipulator,evt_options);
}
})();
//console.table({'PointerEvtTypesHandler': obj_args});
}
}
export const pointerEvtTypesHandler = async (obj_args)=>{
return new PointerEvtTypesHandler(obj_args);
};3: (functions that I use all over the place)
export const addClass = async (...args)=>{
const [elem,add_class,log = false]= args;
let el;
if(null !== elem){
el = elem;
if(!el.classList.contains(add_class)){
el.classList.add(add_class);
}
if(log === true) console.log(`class ${add_class} added to: ${el}`);
}
return await el;
};
export const addClasses = async (...args)=>{
const [elem,classes=[]]= args;
let el
if(null !== elem){
for(const cl of classes){
el = await addClass(elem,cl);
}
}
return el;
};
export async function createElem(elem = null){
if(null !== elem){
return await document.createElement(elem);
}
}
export async function createObjects(...args){
const [map_object = null, map_entries = null] = args;
if(map_object !== null && map_entries !== null){
const map = new Map([[map_object,map_entries]]);
return map.get(map_object);
}
return null;
};
export const getBoundings = async (...args) =>{
const [elem = null] = args;
if(elem !== null){
return elem.getBoundingClientRect();
}
return;
};
export const getTagNames = async (...args) => {
const [tag, parent_el = null,log = false] = args
let el;
if(parent_el !== null){
el = parent_el.getElementsByTagName(tag);
}else{
el = document.getElementsByTagName(tag);
}
if(log === true){
console.log(`getTagNames(${tag})`,el);
}
return el;
}
export const uniqueArray = (array) => Array.from(new Set(array));4: (It isn't that much and could be included in a already existed sylesheet too!)
.sticky{
position: sticky;
inset: 0;
}
tooltip-ctn{
padding: 0.15rem 0.125rem;
height: fit-content;
width: fit-content;
font-size: 0.65rem;
color: #254f4b;
background: #f9f6bd;
border:1px solid;
border-radius: 0.2rem;
white-space: nowrap;
z-index: 100;
}5: (next is a custom element and should be present in the js stack!)
//tooltipModule/tooltip_ctn.js
class TooltipCtn extends HTMLElement{
constructor() {
super();
}
}
export function tooltipCtnDefine(){
customElements.define("tooltip-ctn", TooltipCtn);
}6: (tooltipModule)
//tooltipModule/tooltip_module.js
import * as MFT from './factory/mdl_functions.js';
import {tooltipCreate} from './tooltip_create.js';
class TooltipModule{
#added_tags;
get_tags;
body;
constructor(...args){
const [added_tags] = args;
this.#added_tags = added_tags ?? null;
this.body = document.body;
(async()=> {
if(this.#added_tags !== null){
this.get_tags = await MFT.getTagNames('*',this.#added_tags);
}else
this.get_tags = await MFT.getTagNames('*',this.body);
if(this.get_tags.length > 0){
for(const tag_elem of MFT.uniqueArray(this.get_tags)){
await tooltipCreate(tag_elem,this.body);
}
}
})();
}
}
export const tooltipModule = async (...args)=>{
return new TooltipModule(...args);
}7: (tooltipCreate)
//tooltipModule/tooltip_create.js
import * as MFT from './factory/mdl_functions.js';
import {tooltipActions} from './tooltip_actions.js';
class TooltipCreate{
#body_el;
#parent_el;
create_elem;
constructor(...args){
const[parent_el,body_el] = args;
this.#body_el = body_el;
this.#parent_el = parent_el ?? null;
(async()=> {
this.create_elem = await MFT.createElem('tooltip-ctn');
this.create_elem.cloneNode(true);
this.create_elem.role = 'tooltip';
await MFT.addClasses(this.create_elem,['tooltip-ctn','fixed','display-inline']);
if(this.#parent_el !== null && this.#parent_el.hasAttribute('title')){
await tooltipActions(this.#parent_el,this.create_elem,this.#body_el);
}
})();
//console.table({'TooltipCreate': args});
}
}
export const tooltipCreate = async (...args)=>{
return new TooltipCreate(...args);
}8: (tooltipActions)
//tooltipModule/tooltip_actions.js
import * as MFT from './factory/mdl_functions.js';
import {pointerEvtTypesHandler} from './factory/handlers/pointer_evt_types_handler.js';
class TooltipActions{
#body_el;
#tooltip_el;
#parent_el;
tt_down_enter_move;
tt_out_up;
constructor(...args){
const[parent_el,tooltip_el,body_el] = args;
this.#parent_el = parent_el ?? null;
this.#tooltip_el = tooltip_el;
this.#body_el = body_el;
(async()=> {
if(this.#parent_el !== null){
const pointer_data = await MFT.createObjects('pointer_obj',{
parent_el: this.#parent_el
});
pointer_data.down = {
evt_type: 'pointerdown',
evt_cb_mouse: (...args)=>{
const [evt,evt_target] = args;
/**
* pointerType.mouse I only use for testing
* as this module is module devices only
*/
this.tt_down_enter_move(this.#parent_el,this.#tooltip_el,evt,evt_target);
},
evt_cb_pen: (...args)=>{
const [evt,evt_target] = args;
this.tt_down_enter_move(this.#parent_el,this.#tooltip_el,evt,evt_target);
},
evt_cb_touch: (...args)=>{
const [evt,evt_target] = args;
this.tt_down_enter_move(this.#parent_el,this.#tooltip_el,evt,evt_target);
},
prevent_default: false,
};
pointer_data.enter = {
evt_type: 'pointerenter',
evt_cb_mouse: (...args)=>{
const [evt,evt_target] = args;
/**
* pointerType.mouse I only use for testing
* as this module is module devices only
*/
this.tt_down_enter_move(this.#parent_el,this.#tooltip_el,evt,evt_target);
},
evt_cb_pen: (...args)=>{
const [evt,evt_target] = args;
this.tt_down_enter_move(this.#parent_el,this.#tooltip_el,evt,evt_target);
},
evt_cb_touch: (...args)=>{
const [evt,evt_target] = args;
this.tt_down_enter_move(this.#parent_el,this.#tooltip_el,evt,evt_target);
},
};
pointer_data.move = {
evt_type: 'pointermove',
evt_cb_mouse: (...args)=>{
const [evt,evt_target] = args;
/**
* pointerType.mouse I only use for testing
* as this module is module devices only
*/
this.tt_down_enter_move(this.#parent_el,this.#tooltip_el,evt,evt_target);
},
evt_cb_pen: (...args)=>{
const [evt,evt_target] = args;
this.tt_down_enter_move(this.#parent_el,this.#tooltip_el,evt,evt_target);
},
evt_cb_touch: (...args)=>{
const [evt,evt_target] = args;
this.tt_down_enter_move(this.#parent_el,this.#tooltip_el,evt,evt_target);
},
};
pointer_data.out = {
evt_type: 'pointerout',
evt_cb_mouse: (...args)=>{
const [evt,evt_target] = args;
/**
* pointerType.mouse I only use for testing
* as this module is module devices only
*/
this.tt_out_up(this.#parent_el,this.#tooltip_el,evt,evt_target);
},
evt_cb_pen: (...args)=>{
const [evt,evt_target] = args;
this.tt_out_up(this.#parent_el,this.#tooltip_el,evt,evt_target);
},
evt_cb_touch: (...args)=>{
const [evt,evt_target] = args;
this.tt_out_up(this.#parent_el,this.#tooltip_el,evt,evt_target);
},
};
pointer_data.up = {
evt_type: 'pointerup',
evt_cb_mouse: (...args)=>{
const [evt,evt_target] = args;
/**
* pointerType.mouse I only use for testing
* as this module is module devices only
*/
this.tt_out_up(this.#parent_el,this.#tooltip_el,evt,evt_target);
},
evt_cb_pen: (...args)=>{
const [evt,evt_target] = args;
this.tt_out_up(this.#parent_el,this.#tooltip_el,evt,evt_target);
},
evt_cb_touch: (...args)=>{
const [evt,evt_target] = args;
this.tt_out_up(this.#parent_el,this.#tooltip_el,evt,evt_target);
},
};
const {parent_el,down,enter,move,out,up} = pointer_data;
//here I smuggle a bit, because I have this on the todo list
await Promise.all([
pointerEvtTypesHandler({parent_el,...down}),
pointerEvtTypesHandler({parent_el,...enter}),
pointerEvtTypesHandler({parent_el,...move}),
pointerEvtTypesHandler({parent_el,...out}),
pointerEvtTypesHandler({parent_el,...up})
]);
}
})();
}
tt_down_enter_move = (...args)=>{
const [parent_el,tooltip_el,pointer_evt,evt_target] = args;
(async()=> {
const tt_data = await MFT.createObjects('tt_obj',{});
if(evt_target.hasAttribute('title')){
const body_data = await MFT.getBoundings(this.#body_el);
tt_data.bd = {
bd_width: body_data.width,
bd_right: body_data.right,
};
const {bd_width,bd_right} = tt_data.bd;
tt_data.evt = {
evt_sX: pointer_evt.screenX,
evt_sY: pointer_evt.screenY,
evt_cX: pointer_evt.clientX,
evt_cY: pointer_evt.clientY,
};
const {evt_sX,evt_sY,evt_cX,evt_cY} = tt_data.evt;
const tooltip_el_data = await MFT.getBoundings(tooltip_el);
tt_data.ted = {
ted_width: tooltip_el_data.width,
ted_height: tooltip_el_data.height,
ted_right: tooltip_el_data.right,
};
const {ted_width,ted_height,ted_right} = tt_data.ted;
tooltip_el.textContent = evt_target.title;
const container = this.#body_el.firstElementChild;
this.#body_el.insertBefore(tooltip_el,container);
const target_left1 = evt_sX - evt_cX;
let to_right = target_left1;
if(ted_right >= bd_right){
to_right = target_left1 +(ted_right - bd_right);
}else{
to_right = (target_left1 - 5);
}
const target_left2 = evt_sX - to_right;
tooltip_el.style.left = `${target_left2}px`;
const target_height = evt_target.offsetHeight;
const target_top1 = evt_sY - evt_cY;
const target_top2 = evt_sY - (target_top1 + target_height + 5);
tooltip_el.style.top = `${target_top2}px`;
}
})();
}
tt_out_up = (...args)=>{
const [parent_el,tooltip_el = null,tooltip_evt,evt_target] = args;
(async()=> {
if(parent_el.hasAttribute('title')){
if(tooltip_el !== null){
tooltip_el.replaceWith('');
}
}
})();
}
}
export const tooltipActions = async (...args)=>{
return new TooltipActions(...args);
}It's not complicated but there are some things to notice!\
- Initial and after you have imported this module
- await tooltipModule(); 1
- If there are callbacks who add new elements to the domtree, they have to be added too!
- await tooltipModule(parent_el); 2
// getting the parent of the added elements
const parent_el = document.getElementById('parent_id');
// or
const parent_el = title_elem.parentElement;
await tooltipModule(parent_el); ☛ 1: Here the 'tooltip module' gets all the tags of the html page at the time of invoking but has no knowledge about the elements that are appended after.
☛ 2: That's why the 'tooltip module' has to be re-invoked when new elements are appended to the domtree.
- js: 16kb
- css: 1kb