mirror of
https://git.datalinker.icu/ltdrdata/ComfyUI-Manager
synced 2025-12-08 21:54:26 +08:00
620 lines
17 KiB
JavaScript
620 lines
17 KiB
JavaScript
const hasOwn = function(obj, key) {
|
|
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
};
|
|
|
|
const isNum = function(num) {
|
|
if (typeof num !== 'number' || isNaN(num)) {
|
|
return false;
|
|
}
|
|
const isInvalid = function(n) {
|
|
if (n === Number.MAX_VALUE || n === Number.MIN_VALUE || n === Number.NEGATIVE_INFINITY || n === Number.POSITIVE_INFINITY) {
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
if (isInvalid(num)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
const toNum = (num) => {
|
|
if (typeof (num) !== 'number') {
|
|
num = parseFloat(num);
|
|
}
|
|
if (isNaN(num)) {
|
|
num = 0;
|
|
}
|
|
num = Math.round(num);
|
|
return num;
|
|
};
|
|
|
|
const clamp = function(value, min, max) {
|
|
return Math.max(min, Math.min(max, value));
|
|
};
|
|
|
|
const isWindow = (obj) => {
|
|
return Boolean(obj && obj === obj.window);
|
|
};
|
|
|
|
const isDocument = (obj) => {
|
|
return Boolean(obj && obj.nodeType === 9);
|
|
};
|
|
|
|
const isElement = (obj) => {
|
|
return Boolean(obj && obj.nodeType === 1);
|
|
};
|
|
|
|
// ===========================================================================================
|
|
|
|
export const toRect = (obj) => {
|
|
if (obj) {
|
|
return {
|
|
left: toNum(obj.left || obj.x),
|
|
top: toNum(obj.top || obj.y),
|
|
width: toNum(obj.width),
|
|
height: toNum(obj.height)
|
|
};
|
|
}
|
|
return {
|
|
left: 0,
|
|
top: 0,
|
|
width: 0,
|
|
height: 0
|
|
};
|
|
};
|
|
|
|
export const getElement = (selector) => {
|
|
if (typeof selector === 'string' && selector) {
|
|
if (selector.startsWith('#')) {
|
|
return document.getElementById(selector.slice(1));
|
|
}
|
|
return document.querySelector(selector);
|
|
}
|
|
|
|
if (isDocument(selector)) {
|
|
return selector.body;
|
|
}
|
|
if (isElement(selector)) {
|
|
return selector;
|
|
}
|
|
};
|
|
|
|
export const getRect = (target, fixed) => {
|
|
if (!target) {
|
|
return toRect();
|
|
}
|
|
|
|
if (isWindow(target)) {
|
|
return {
|
|
left: 0,
|
|
top: 0,
|
|
width: window.innerWidth,
|
|
height: window.innerHeight
|
|
};
|
|
}
|
|
|
|
const elem = getElement(target);
|
|
if (!elem) {
|
|
return toRect(target);
|
|
}
|
|
|
|
const br = elem.getBoundingClientRect();
|
|
const rect = toRect(br);
|
|
|
|
// fix offset
|
|
if (!fixed) {
|
|
rect.left += window.scrollX;
|
|
rect.top += window.scrollY;
|
|
}
|
|
|
|
rect.width = elem.offsetWidth;
|
|
rect.height = elem.offsetHeight;
|
|
|
|
return rect;
|
|
};
|
|
|
|
// ===========================================================================================
|
|
|
|
const calculators = {
|
|
|
|
bottom: (info, containerRect, targetRect) => {
|
|
info.space = containerRect.top + containerRect.height - targetRect.top - targetRect.height - info.height;
|
|
info.top = targetRect.top + targetRect.height;
|
|
info.left = Math.round(targetRect.left + targetRect.width * 0.5 - info.width * 0.5);
|
|
},
|
|
|
|
top: (info, containerRect, targetRect) => {
|
|
info.space = targetRect.top - info.height - containerRect.top;
|
|
info.top = targetRect.top - info.height;
|
|
info.left = Math.round(targetRect.left + targetRect.width * 0.5 - info.width * 0.5);
|
|
},
|
|
|
|
right: (info, containerRect, targetRect) => {
|
|
info.space = containerRect.left + containerRect.width - targetRect.left - targetRect.width - info.width;
|
|
info.top = Math.round(targetRect.top + targetRect.height * 0.5 - info.height * 0.5);
|
|
info.left = targetRect.left + targetRect.width;
|
|
},
|
|
|
|
left: (info, containerRect, targetRect) => {
|
|
info.space = targetRect.left - info.width - containerRect.left;
|
|
info.top = Math.round(targetRect.top + targetRect.height * 0.5 - info.height * 0.5);
|
|
info.left = targetRect.left - info.width;
|
|
}
|
|
};
|
|
|
|
// with order
|
|
export const getDefaultPositions = () => {
|
|
return Object.keys(calculators);
|
|
};
|
|
|
|
const calculateSpace = (info, containerRect, targetRect) => {
|
|
const calculator = calculators[info.position];
|
|
calculator(info, containerRect, targetRect);
|
|
if (info.space >= 0) {
|
|
info.passed += 1;
|
|
}
|
|
};
|
|
|
|
// ===========================================================================================
|
|
|
|
const calculateAlignOffset = (info, containerRect, targetRect, alignType, sizeType) => {
|
|
|
|
const popoverStart = info[alignType];
|
|
const popoverSize = info[sizeType];
|
|
|
|
const containerStart = containerRect[alignType];
|
|
const containerSize = containerRect[sizeType];
|
|
|
|
const targetStart = targetRect[alignType];
|
|
const targetSize = targetRect[sizeType];
|
|
|
|
const targetCenter = targetStart + targetSize * 0.5;
|
|
|
|
// size overflow
|
|
if (popoverSize > containerSize) {
|
|
const overflow = (popoverSize - containerSize) * 0.5;
|
|
info[alignType] = containerStart - overflow;
|
|
info.offset = targetCenter - containerStart + overflow;
|
|
return;
|
|
}
|
|
|
|
const space1 = popoverStart - containerStart;
|
|
const space2 = (containerStart + containerSize) - (popoverStart + popoverSize);
|
|
|
|
// both side passed, default to center
|
|
if (space1 >= 0 && space2 >= 0) {
|
|
if (info.passed) {
|
|
info.passed += 2;
|
|
}
|
|
info.offset = popoverSize * 0.5;
|
|
return;
|
|
}
|
|
|
|
// one side passed
|
|
if (info.passed) {
|
|
info.passed += 1;
|
|
}
|
|
|
|
if (space1 < 0) {
|
|
const min = containerStart;
|
|
info[alignType] = min;
|
|
info.offset = targetCenter - min;
|
|
return;
|
|
}
|
|
|
|
// space2 < 0
|
|
const max = containerStart + containerSize - popoverSize;
|
|
info[alignType] = max;
|
|
info.offset = targetCenter - max;
|
|
|
|
};
|
|
|
|
const calculateHV = (info, containerRect) => {
|
|
if (['top', 'bottom'].includes(info.position)) {
|
|
info.top = clamp(info.top, containerRect.top, containerRect.top + containerRect.height - info.height);
|
|
return ['left', 'width'];
|
|
}
|
|
info.left = clamp(info.left, containerRect.left, containerRect.left + containerRect.width - info.width);
|
|
return ['top', 'height'];
|
|
};
|
|
|
|
const calculateOffset = (info, containerRect, targetRect) => {
|
|
|
|
const [alignType, sizeType] = calculateHV(info, containerRect);
|
|
|
|
calculateAlignOffset(info, containerRect, targetRect, alignType, sizeType);
|
|
|
|
info.offset = clamp(info.offset, 0, info[sizeType]);
|
|
|
|
};
|
|
|
|
// ===========================================================================================
|
|
|
|
const calculateDistance = (info, previousPositionInfo) => {
|
|
if (!previousPositionInfo) {
|
|
return;
|
|
}
|
|
// no change if position no change with previous
|
|
if (info.position === previousPositionInfo.position) {
|
|
return;
|
|
}
|
|
const ax = info.left + info.width * 0.5;
|
|
const ay = info.top + info.height * 0.5;
|
|
const bx = previousPositionInfo.left + previousPositionInfo.width * 0.5;
|
|
const by = previousPositionInfo.top + previousPositionInfo.height * 0.5;
|
|
const dx = Math.abs(ax - bx);
|
|
const dy = Math.abs(ay - by);
|
|
info.distance = Math.round(Math.sqrt(dx * dx + dy * dy));
|
|
};
|
|
|
|
// ===========================================================================================
|
|
|
|
const calculatePositionInfo = (info, containerRect, targetRect, previousPositionInfo) => {
|
|
calculateSpace(info, containerRect, targetRect);
|
|
calculateOffset(info, containerRect, targetRect);
|
|
calculateDistance(info, previousPositionInfo);
|
|
};
|
|
|
|
// ===========================================================================================
|
|
|
|
const calculateBestPosition = (containerRect, targetRect, infoMap, withOrder, previousPositionInfo) => {
|
|
|
|
// position space: +1
|
|
// align space:
|
|
// two side passed: +2
|
|
// one side passed: +1
|
|
|
|
const safePassed = 3;
|
|
|
|
if (previousPositionInfo) {
|
|
const prevInfo = infoMap[previousPositionInfo.position];
|
|
if (prevInfo) {
|
|
calculatePositionInfo(prevInfo, containerRect, targetRect);
|
|
if (prevInfo.passed >= safePassed) {
|
|
return prevInfo;
|
|
}
|
|
prevInfo.calculated = true;
|
|
}
|
|
}
|
|
|
|
const positionList = [];
|
|
Object.values(infoMap).forEach((info) => {
|
|
if (!info.calculated) {
|
|
calculatePositionInfo(info, containerRect, targetRect, previousPositionInfo);
|
|
}
|
|
positionList.push(info);
|
|
});
|
|
|
|
positionList.sort((a, b) => {
|
|
if (a.passed !== b.passed) {
|
|
return b.passed - a.passed;
|
|
}
|
|
|
|
if (withOrder && a.passed >= safePassed && b.passed >= safePassed) {
|
|
return a.index - b.index;
|
|
}
|
|
|
|
if (a.space !== b.space) {
|
|
return b.space - a.space;
|
|
}
|
|
|
|
return a.index - b.index;
|
|
});
|
|
|
|
// logTable(positionList);
|
|
|
|
return positionList[0];
|
|
};
|
|
|
|
// const logTable = (() => {
|
|
// let time_id;
|
|
// return (info) => {
|
|
// clearTimeout(time_id);
|
|
// time_id = setTimeout(() => {
|
|
// console.table(info);
|
|
// }, 10);
|
|
// };
|
|
// })();
|
|
|
|
// ===========================================================================================
|
|
|
|
const getAllowPositions = (positions, defaultAllowPositions) => {
|
|
if (!positions) {
|
|
return;
|
|
}
|
|
if (Array.isArray(positions)) {
|
|
positions = positions.join(',');
|
|
}
|
|
positions = String(positions).split(',').map((it) => it.trim().toLowerCase()).filter((it) => it);
|
|
positions = positions.filter((it) => defaultAllowPositions.includes(it));
|
|
if (!positions.length) {
|
|
return;
|
|
}
|
|
return positions;
|
|
};
|
|
|
|
const isPositionChanged = (info, previousPositionInfo) => {
|
|
if (!previousPositionInfo) {
|
|
return true;
|
|
}
|
|
|
|
if (info.left !== previousPositionInfo.left) {
|
|
return true;
|
|
}
|
|
|
|
if (info.top !== previousPositionInfo.top) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
// ===========================================================================================
|
|
|
|
// const log = (name, time) => {
|
|
// if (time > 0.1) {
|
|
// console.log(name, time);
|
|
// }
|
|
// };
|
|
|
|
export const getBestPosition = (containerRect, targetRect, popoverRect, positions, previousPositionInfo) => {
|
|
|
|
const defaultAllowPositions = getDefaultPositions();
|
|
let withOrder = true;
|
|
let allowPositions = getAllowPositions(positions, defaultAllowPositions);
|
|
if (!allowPositions) {
|
|
allowPositions = defaultAllowPositions;
|
|
withOrder = false;
|
|
}
|
|
|
|
// console.log('withOrder', withOrder);
|
|
|
|
// const start_time = performance.now();
|
|
|
|
const infoMap = {};
|
|
allowPositions.forEach((k, i) => {
|
|
infoMap[k] = {
|
|
position: k,
|
|
index: i,
|
|
|
|
top: 0,
|
|
left: 0,
|
|
width: popoverRect.width,
|
|
height: popoverRect.height,
|
|
|
|
space: 0,
|
|
|
|
offset: 0,
|
|
passed: 0,
|
|
|
|
distance: 0
|
|
};
|
|
});
|
|
|
|
// log('infoMap', performance.now() - start_time);
|
|
|
|
|
|
const bestPosition = calculateBestPosition(containerRect, targetRect, infoMap, withOrder, previousPositionInfo);
|
|
|
|
// check left/top
|
|
bestPosition.changed = isPositionChanged(bestPosition, previousPositionInfo);
|
|
|
|
return bestPosition;
|
|
};
|
|
|
|
// ===========================================================================================
|
|
|
|
const getTemplatePath = (width, height, arrowOffset, arrowSize, borderRadius) => {
|
|
const p = (px, py) => {
|
|
return [px, py].join(',');
|
|
};
|
|
|
|
const px = function(num, alignEnd) {
|
|
const floor = Math.floor(num);
|
|
let n = num < floor + 0.5 ? floor + 0.5 : floor + 1.5;
|
|
if (alignEnd) {
|
|
n -= 1;
|
|
}
|
|
return n;
|
|
};
|
|
|
|
const pxe = function(num) {
|
|
return px(num, true);
|
|
};
|
|
|
|
const ls = [];
|
|
|
|
const innerLeft = px(arrowSize);
|
|
const innerRight = pxe(width - arrowSize);
|
|
arrowOffset = clamp(arrowOffset, innerLeft, innerRight);
|
|
|
|
const innerTop = px(arrowSize);
|
|
const innerBottom = pxe(height - arrowSize);
|
|
|
|
const startPoint = p(innerLeft, innerTop + borderRadius);
|
|
const arrowPoint = p(arrowOffset, 1);
|
|
|
|
const LT = p(innerLeft, innerTop);
|
|
const RT = p(innerRight, innerTop);
|
|
|
|
const AOT = p(arrowOffset - arrowSize, innerTop);
|
|
const RRT = p(innerRight - borderRadius, innerTop);
|
|
|
|
ls.push(`M${startPoint}`);
|
|
ls.push(`V${innerBottom - borderRadius}`);
|
|
ls.push(`Q${p(innerLeft, innerBottom)} ${p(innerLeft + borderRadius, innerBottom)}`);
|
|
ls.push(`H${innerRight - borderRadius}`);
|
|
ls.push(`Q${p(innerRight, innerBottom)} ${p(innerRight, innerBottom - borderRadius)}`);
|
|
ls.push(`V${innerTop + borderRadius}`);
|
|
|
|
if (arrowOffset < innerLeft + arrowSize + borderRadius) {
|
|
ls.push(`Q${RT} ${RRT}`);
|
|
ls.push(`H${arrowOffset + arrowSize}`);
|
|
ls.push(`L${arrowPoint}`);
|
|
if (arrowOffset < innerLeft + arrowSize) {
|
|
ls.push(`L${LT}`);
|
|
ls.push(`L${startPoint}`);
|
|
} else {
|
|
ls.push(`L${AOT}`);
|
|
ls.push(`Q${LT} ${startPoint}`);
|
|
}
|
|
} else if (arrowOffset > innerRight - arrowSize - borderRadius) {
|
|
if (arrowOffset > innerRight - arrowSize) {
|
|
ls.push(`L${RT}`);
|
|
} else {
|
|
ls.push(`Q${RT} ${p(arrowOffset + arrowSize, innerTop)}`);
|
|
}
|
|
ls.push(`L${arrowPoint}`);
|
|
ls.push(`L${AOT}`);
|
|
ls.push(`H${innerLeft + borderRadius}`);
|
|
ls.push(`Q${LT} ${startPoint}`);
|
|
} else {
|
|
ls.push(`Q${RT} ${RRT}`);
|
|
ls.push(`H${arrowOffset + arrowSize}`);
|
|
ls.push(`L${arrowPoint}`);
|
|
ls.push(`L${AOT}`);
|
|
ls.push(`H${innerLeft + borderRadius}`);
|
|
ls.push(`Q${LT} ${startPoint}`);
|
|
}
|
|
return ls.join('');
|
|
};
|
|
|
|
const getPathData = function(position, width, height, arrowOffset, arrowSize, borderRadius) {
|
|
|
|
const handlers = {
|
|
|
|
bottom: () => {
|
|
const d = getTemplatePath(width, height, arrowOffset, arrowSize, borderRadius);
|
|
return {
|
|
d,
|
|
transform: ''
|
|
};
|
|
},
|
|
|
|
top: () => {
|
|
const d = getTemplatePath(width, height, width - arrowOffset, arrowSize, borderRadius);
|
|
return {
|
|
d,
|
|
transform: `rotate(180,${width * 0.5},${height * 0.5})`
|
|
};
|
|
},
|
|
|
|
left: () => {
|
|
const d = getTemplatePath(height, width, arrowOffset, arrowSize, borderRadius);
|
|
const x = (width - height) * 0.5;
|
|
const y = (height - width) * 0.5;
|
|
return {
|
|
d,
|
|
transform: `translate(${x} ${y}) rotate(90,${height * 0.5},${width * 0.5})`
|
|
};
|
|
},
|
|
|
|
right: () => {
|
|
const d = getTemplatePath(height, width, height - arrowOffset, arrowSize, borderRadius);
|
|
const x = (width - height) * 0.5;
|
|
const y = (height - width) * 0.5;
|
|
return {
|
|
d,
|
|
transform: `translate(${x} ${y}) rotate(-90,${height * 0.5},${width * 0.5})`
|
|
};
|
|
}
|
|
};
|
|
|
|
return handlers[position]();
|
|
};
|
|
|
|
// ===========================================================================================
|
|
|
|
// position style cache
|
|
const styleCache = {
|
|
// position: '',
|
|
// top: {},
|
|
// bottom: {},
|
|
// left: {},
|
|
// right: {}
|
|
};
|
|
|
|
export const getPositionStyle = (info, options = {}) => {
|
|
|
|
const o = {
|
|
bgColor: '#fff',
|
|
borderColor: '#ccc',
|
|
borderRadius: 5,
|
|
arrowSize: 10
|
|
};
|
|
Object.keys(o).forEach((k) => {
|
|
|
|
if (hasOwn(options, k)) {
|
|
const d = o[k];
|
|
const v = options[k];
|
|
|
|
if (typeof d === 'string') {
|
|
// string
|
|
if (typeof v === 'string' && v) {
|
|
o[k] = v;
|
|
}
|
|
} else {
|
|
// number
|
|
if (isNum(v) && v >= 0) {
|
|
o[k] = v;
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
});
|
|
|
|
const key = [
|
|
info.width,
|
|
info.height,
|
|
info.offset,
|
|
o.arrowSize,
|
|
o.borderRadius,
|
|
o.bgColor,
|
|
o.borderColor
|
|
].join('-');
|
|
|
|
const positionCache = styleCache[info.position];
|
|
if (positionCache && key === positionCache.key) {
|
|
const st = positionCache.style;
|
|
st.changed = styleCache.position !== info.position;
|
|
styleCache.position = info.position;
|
|
return st;
|
|
}
|
|
|
|
// console.log(options);
|
|
|
|
const data = getPathData(info.position, info.width, info.height, info.offset, o.arrowSize, o.borderRadius);
|
|
// console.log(data);
|
|
|
|
const viewBox = [0, 0, info.width, info.height].join(' ');
|
|
const svg = [
|
|
`<svg viewBox="${viewBox}" xmlns="http://www.w3.org/2000/svg">`,
|
|
`<path d="${data.d}" fill="${o.bgColor}" stroke="${o.borderColor}" transform="${data.transform}" />`,
|
|
'</svg>'
|
|
].join('');
|
|
|
|
// console.log(svg);
|
|
const backgroundImage = `url("data:image/svg+xml;charset=utf8,${encodeURIComponent(svg)}")`;
|
|
|
|
const background = `${backgroundImage} center no-repeat`;
|
|
|
|
const padding = `${o.arrowSize + o.borderRadius}px`;
|
|
|
|
const style = {
|
|
background,
|
|
backgroundImage,
|
|
padding,
|
|
changed: true
|
|
};
|
|
|
|
styleCache.position = info.position;
|
|
styleCache[info.position] = {
|
|
key,
|
|
style
|
|
};
|
|
|
|
return style;
|
|
};
|