Compare commits

...

7 Commits

Author SHA1 Message Date
pk
de558561b6 Move code to make sure a clean-up happens on each run. 2025-07-06 00:07:53 +02:00
pk
ab914d8bf2 Improve _posFixed to make sure content never gets cut off on very small view ports. 2025-07-05 23:49:12 +02:00
pk
e75b7b8efe Bump version 2025-03-25 12:51:47 +01:00
pk
28fbac0c6d Add waitFor 2025-03-25 12:51:19 +01:00
pk
9164bae268 Add color functions 2025-02-06 22:24:49 +01:00
pk
0c154c23d0 Add visibility mixin 2025-01-26 13:43:30 +01:00
pk
9c1ee77a20 Files should be an empty array by default 2025-01-17 11:08:15 +01:00
6 changed files with 174 additions and 2 deletions

75
color.js Normal file
View File

@ -0,0 +1,75 @@
/**
* Calculate light or darker color based on the luminance arg.
*
* @param {String} hex Color hex string to calculate on.
* @param {Number} lum Luminance.
* @return {String} The new color hex string.
*/
export const colorLuminance = (hex, lum) => {
// validate hex string
hex = String(hex).replace(/[^0-9a-f]/gi, '');
if (hex.length < 6) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
lum = lum || 0;
// convert to decimal and change luminosity
let rgb = '#';
let c;
let i;
for (i = 0; i < 3; i++) {
c = parseInt(hex.substr(i * 2, 2), 16);
c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16);
rgb += ('00' + c).substr(c.length);
}
return rgb;
}
/**
* Calculate an appropriate contrast color.
*
* @param {String} hex Hex string of the color we're searching for the right contrast.
* @param {String} dark Dark color to return. Defaults to #000000.
* @param {String} light Light color to return. Defaults to #ffffff.
* @return {String} Hex color string with the appropriate contrast color.
*/
export const contrastTextColor = (hex, dark, light) => {
dark = dark || '#000000';
light = light || '#ffffff';
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (!result) {
return dark;
}
const red = parseInt(result[1], 16);
const green = parseInt(result[2], 16);
const blue = parseInt(result[3], 16);
let brightness;
brightness = (red * 299) + (green * 587) + (blue * 114);
brightness /= 255000;
// values range from 0 to 1
// anything greater than 0.6 should be bright enough for dark text
if (brightness >= 0.6) {
return dark;
}
return light;
}
/**
* Convert rgb(r, g, b) to hex.
*
* @param {String} rgb Rgb string to convert.
* @return {String} The hex string.
*/
export const rgbToHex = rgb => {
const matches = rgb.match(/rgb\(([0-9]+), ([0-9]+), ([0-9]+)\)/);
if (matches.length !== 4) {
return '';
}
// From https://gist.github.com/lrvick/2080648.
const bin = matches[1] << 16 | matches[2] << 8 | matches[3];
return (function(h) {
return '#' + new Array(7 - h.length).join('0') + h;
})(bin.toString(16).toLowerCase());
}

View File

@ -1,6 +1,6 @@
{ {
"name": "@tp/helpers", "name": "@tp/helpers",
"version": "2.3.1", "version": "2.7.1",
"description": "", "description": "",
"main": "closest.js", "main": "closest.js",
"scripts": { "scripts": {

View File

@ -21,6 +21,10 @@ export const Position = function(superClass) {
el.style.position = 'fixed'; el.style.position = 'fixed';
el.style.zIndex = 1001; el.style.zIndex = 1001;
// Reset max-height and overflow in case the element has been positioned before.
el.style.maxHeight = '';
el.style.overflowY = '';
// Test if the target is in a different stacking context. // Test if the target is in a different stacking context.
el.style.left = '0px'; el.style.left = '0px';
el.style.top = '0px'; el.style.top = '0px';
@ -52,6 +56,12 @@ export const Position = function(superClass) {
} }
} }
// Ensure top never goes below 0
if (top < 0) {
compTop += Math.abs(top);
top = 0;
}
if (options.halign === 'left') { if (options.halign === 'left') {
left = anchorRect.left; left = anchorRect.left;
} }
@ -74,6 +84,13 @@ export const Position = function(superClass) {
left = 0; left = 0;
} }
// Constrain height to viewport - calculate available height from final top position
const availableHeight = window.innerHeight - top;
if (elRect.height > availableHeight) {
el.style.maxHeight = availableHeight + 'px';
el.style.overflowY = 'auto';
}
el.style.top = (top - fixTop) + 'px'; el.style.top = (top - fixTop) + 'px';
el.style.left = (left - fixLeft) + 'px'; el.style.left = (left - fixLeft) + 'px';

View File

@ -8,7 +8,7 @@ export const upload = function(superClass) {
* @param {Object} opts Upload options * @param {Object} opts Upload options
* @returns Promise * @returns Promise
*/ */
uploadFiles(url, files, data = {}) { uploadFiles(url, files = [], data = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const request = new XMLHttpRequest(); const request = new XMLHttpRequest();
const formData = new FormData(); const formData = new FormData();

52
visibility.js Normal file
View File

@ -0,0 +1,52 @@
/**
@license
Copyright (c) 2025 trading_peter
This program is available under Apache License Version 2.0
*/
/**
* Orchastrates a intersetion observer to execute code if the element becomes visible or hidden/disconnected.
*/
export const Visibility = function(superClass) {
return class extends superClass {
constructor() {
super();
// Create intersection observer
this._observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.visibilityChanged(true);
} else {
this.visibilityChanged(false);
}
});
});
}
/**
* Invoked when the element becomes either visible / connected or hidden / disconnected.
* The callback's logic must be able to handle multiple, successive calls with the same state.
* @param {boolean} visible
*/
visibilityChanged(visible) {
console.warn(this.tagName, 'should override onVisible');
}
connectedCallback() {
super.connectedCallback();
// Start observing this element
this._observer.observe(this);
}
disconnectedCallback() {
super.disconnectedCallback();
// Clean up
this._observer.unobserve(this);
this._observer.disconnect();
this.visibilityChanged(false);
}
};
}

28
wait-for.js Normal file
View File

@ -0,0 +1,28 @@
/**
* Wait for some test to be true, returns a promise that resolves when condition is met.
* Useful to wait for element render, for example. Uses requestAnimationFrame.
*
* @param {Function} testFn Test function to call. Should return true to resolve and false to continue testing.
* @param {Number} maxTries Maximum number of rounds to test. If exceeded, the promise rejects. Defaults to 5000.
* @return {Promise} Promise that resolves when test passes or rejects when maxTries is reached
*/
export const waitFor = (testFn, maxTries = 5000) => {
return new Promise((resolve, reject) => {
let tries = 0;
const boundTestFn = testFn.bind(this);
function waiter() {
if (tries === maxTries) {
return reject(new Error('waitFor: maximum tries exceeded'));
}
if (!boundTestFn()) {
window.requestAnimationFrame(waiter);
tries++;
return;
}
resolve(true);
}
waiter();
});
}