tiny-essentials 1.13.2 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/v1/TinyBasicsEs.js +656 -26
- package/dist/v1/TinyBasicsEs.min.js +1 -1
- package/dist/v1/TinyDragger.js +196 -26
- package/dist/v1/TinyEssentials.js +896 -26
- package/dist/v1/TinyEssentials.min.js +1 -1
- package/dist/v1/TinyNotifications.js +408 -0
- package/dist/v1/TinyNotifications.min.js +1 -0
- package/dist/v1/TinyUploadClicker.js +198 -26
- package/dist/v1/basics/collision.cjs +413 -0
- package/dist/v1/basics/collision.d.mts +187 -0
- package/dist/v1/basics/collision.mjs +350 -0
- package/dist/v1/basics/html.cjs +201 -26
- package/dist/v1/basics/html.d.mts +71 -7
- package/dist/v1/basics/html.mjs +186 -23
- package/dist/v1/basics/index.cjs +24 -0
- package/dist/v1/basics/index.d.mts +24 -1
- package/dist/v1/basics/index.mjs +4 -3
- package/dist/v1/basics/text.cjs +43 -0
- package/dist/v1/basics/text.d.mts +16 -0
- package/dist/v1/basics/text.mjs +37 -0
- package/dist/v1/build/TinyNotifications.cjs +7 -0
- package/dist/v1/build/TinyNotifications.d.mts +3 -0
- package/dist/v1/build/TinyNotifications.mjs +2 -0
- package/dist/v1/index.cjs +26 -0
- package/dist/v1/index.d.mts +25 -1
- package/dist/v1/index.mjs +5 -3
- package/dist/v1/libs/TinyNotifications.cjs +238 -0
- package/dist/v1/libs/TinyNotifications.d.mts +106 -0
- package/dist/v1/libs/TinyNotifications.mjs +211 -0
- package/docs/v1/README.md +5 -3
- package/docs/v1/basics/array.md +16 -43
- package/docs/v1/basics/collision.md +237 -0
- package/docs/v1/basics/html.md +117 -28
- package/docs/v1/basics/text.md +44 -14
- package/docs/v1/libs/TinyNotifications.md +189 -0
- package/package.json +5 -2
|
@@ -2890,11 +2890,22 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
2890
2890
|
TinyDragDropDetector: () => (/* reexport */ libs_TinyDragDropDetector),
|
|
2891
2891
|
TinyDragger: () => (/* reexport */ libs_TinyDragger),
|
|
2892
2892
|
TinyLevelUp: () => (/* reexport */ userLevel),
|
|
2893
|
+
TinyNotifications: () => (/* reexport */ libs_TinyNotifications),
|
|
2893
2894
|
TinyNotifyCenter: () => (/* reexport */ libs_TinyNotifyCenter),
|
|
2894
2895
|
TinyPromiseQueue: () => (/* reexport */ libs_TinyPromiseQueue),
|
|
2895
2896
|
TinyRateLimiter: () => (/* reexport */ libs_TinyRateLimiter),
|
|
2896
2897
|
TinyToastNotify: () => (/* reexport */ libs_TinyToastNotify),
|
|
2897
2898
|
addAiMarkerShortcut: () => (/* reexport */ addAiMarkerShortcut),
|
|
2899
|
+
areElsCollBottom: () => (/* reexport */ collision_areElsCollBottom),
|
|
2900
|
+
areElsCollLeft: () => (/* reexport */ collision_areElsCollLeft),
|
|
2901
|
+
areElsCollPerfBottom: () => (/* reexport */ areElsCollPerfBottom),
|
|
2902
|
+
areElsCollPerfLeft: () => (/* reexport */ areElsCollPerfLeft),
|
|
2903
|
+
areElsCollPerfRight: () => (/* reexport */ areElsCollPerfRight),
|
|
2904
|
+
areElsCollPerfTop: () => (/* reexport */ areElsCollPerfTop),
|
|
2905
|
+
areElsCollRight: () => (/* reexport */ collision_areElsCollRight),
|
|
2906
|
+
areElsCollTop: () => (/* reexport */ collision_areElsCollTop),
|
|
2907
|
+
areElsColliding: () => (/* reexport */ collision_areElsColliding),
|
|
2908
|
+
areElsPerfColliding: () => (/* reexport */ areElsPerfColliding),
|
|
2898
2909
|
areHtmlElsColliding: () => (/* reexport */ areHtmlElsColliding),
|
|
2899
2910
|
arraySortPositions: () => (/* reexport */ arraySortPositions),
|
|
2900
2911
|
asyncReplace: () => (/* reexport */ asyncReplace),
|
|
@@ -2922,19 +2933,29 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
2922
2933
|
formatTimer: () => (/* reexport */ formatTimer),
|
|
2923
2934
|
genFibonacciSeq: () => (/* reexport */ genFibonacciSeq),
|
|
2924
2935
|
getAge: () => (/* reexport */ getAge),
|
|
2936
|
+
getElsCollDetails: () => (/* reexport */ getElsCollDetails),
|
|
2937
|
+
getElsCollDirDepth: () => (/* reexport */ getElsCollDirDepth),
|
|
2938
|
+
getElsCollOverlap: () => (/* reexport */ getElsCollOverlap),
|
|
2939
|
+
getElsCollOverlapPos: () => (/* reexport */ getElsCollOverlapPos),
|
|
2940
|
+
getElsColliding: () => (/* reexport */ getElsColliding),
|
|
2941
|
+
getElsPerfColliding: () => (/* reexport */ getElsPerfColliding),
|
|
2942
|
+
getElsRelativeCenterOffset: () => (/* reexport */ getElsRelativeCenterOffset),
|
|
2925
2943
|
getHtmlElBorders: () => (/* reexport */ getHtmlElBorders),
|
|
2926
2944
|
getHtmlElBordersWidth: () => (/* reexport */ getHtmlElBordersWidth),
|
|
2927
2945
|
getHtmlElMargin: () => (/* reexport */ getHtmlElMargin),
|
|
2928
2946
|
getHtmlElPadding: () => (/* reexport */ getHtmlElPadding),
|
|
2929
2947
|
getLatestBackupPath: () => (/* reexport */ normalFuncs_getLatestBackupPath),
|
|
2948
|
+
getRectCenter: () => (/* reexport */ getRectCenter),
|
|
2930
2949
|
getSimplePerc: () => (/* reexport */ getSimplePerc),
|
|
2931
2950
|
getTimeDuration: () => (/* reexport */ getTimeDuration),
|
|
2932
2951
|
installWindowHiddenScript: () => (/* reexport */ installWindowHiddenScript),
|
|
2933
2952
|
isDirEmpty: () => (/* reexport */ isDirEmpty),
|
|
2934
2953
|
isDirEmptyAsync: () => (/* reexport */ isDirEmptyAsync),
|
|
2935
2954
|
isFullScreenMode: () => (/* reexport */ isFullScreenMode),
|
|
2955
|
+
isInViewport: () => (/* reexport */ isInViewport),
|
|
2936
2956
|
isJsonObject: () => (/* reexport */ isJsonObject),
|
|
2937
2957
|
isScreenFilled: () => (/* reexport */ isScreenFilled),
|
|
2958
|
+
isScrolledIntoView: () => (/* reexport */ isScrolledIntoView),
|
|
2938
2959
|
listDirs: () => (/* reexport */ listDirs),
|
|
2939
2960
|
listDirsAsync: () => (/* reexport */ listDirsAsync),
|
|
2940
2961
|
listFiles: () => (/* reexport */ listFiles),
|
|
@@ -2942,6 +2963,8 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
2942
2963
|
objType: () => (/* reexport */ objType),
|
|
2943
2964
|
offFullScreenChange: () => (/* reexport */ offFullScreenChange),
|
|
2944
2965
|
onFullScreenChange: () => (/* reexport */ onFullScreenChange),
|
|
2966
|
+
readBase64Blob: () => (/* reexport */ readBase64Blob),
|
|
2967
|
+
readFileBlob: () => (/* reexport */ readFileBlob),
|
|
2945
2968
|
readJsonBlob: () => (/* reexport */ readJsonBlob),
|
|
2946
2969
|
readJsonFile: () => (/* reexport */ readJsonFile),
|
|
2947
2970
|
renameFileAddPrefixSuffix: () => (/* reexport */ renameFileAddPrefixSuffix),
|
|
@@ -2953,6 +2976,7 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
2953
2976
|
requestFullScreen: () => (/* reexport */ requestFullScreen),
|
|
2954
2977
|
restoreLatestBackup: () => (/* reexport */ restoreLatestBackup),
|
|
2955
2978
|
ruleOfThree: () => (/* reexport */ ruleOfThree),
|
|
2979
|
+
safeTextTrim: () => (/* reexport */ safeTextTrim),
|
|
2956
2980
|
saveJsonFile: () => (/* reexport */ saveJsonFile),
|
|
2957
2981
|
shuffleArray: () => (/* reexport */ shuffleArray),
|
|
2958
2982
|
toTitleCase: () => (/* reexport */ toTitleCase),
|
|
@@ -4236,6 +4260,48 @@ function addAiMarkerShortcut(key = 'a') {
|
|
|
4236
4260
|
});
|
|
4237
4261
|
}
|
|
4238
4262
|
|
|
4263
|
+
/**
|
|
4264
|
+
* Trims a text string to a specified character limit, attempting to avoid cutting words in half.
|
|
4265
|
+
* If a space is found before the limit and it's not too far from the limit (at least 60%),
|
|
4266
|
+
* the cut is made at that space; otherwise, the text is hard-cut at the limit.
|
|
4267
|
+
* If the input is shorter than the limit, it is returned unchanged.
|
|
4268
|
+
*
|
|
4269
|
+
* @param {string} text - The input text to be trimmed.
|
|
4270
|
+
* @param {number} limit - The maximum number of characters allowed.
|
|
4271
|
+
* @param {number} [safeCutZone=0.6] - A decimal between 0 and 1 representing the minimal acceptable position
|
|
4272
|
+
* (as a fraction of `limit`) to cut at a space. Defaults to 0.6.
|
|
4273
|
+
* @returns {string} - The trimmed text, possibly ending with an ellipsis ("...").
|
|
4274
|
+
* @throws {TypeError} - Throws if `text` is not a string.
|
|
4275
|
+
* @throws {TypeError} - Throws if `limit` is not a positive integer.
|
|
4276
|
+
* @throws {TypeError} - Throws if `safeCutZone` is not a number between 0 and 1 (inclusive).
|
|
4277
|
+
*/
|
|
4278
|
+
function safeTextTrim(text, limit, safeCutZone = 0.6) {
|
|
4279
|
+
if (typeof text !== 'string')
|
|
4280
|
+
throw new TypeError(`Expected a string for 'text', but received ${typeof text}`);
|
|
4281
|
+
if (!Number.isInteger(limit) || limit <= 0)
|
|
4282
|
+
throw new TypeError(`Expected 'limit' to be a positive integer, but received ${limit}`);
|
|
4283
|
+
if (typeof safeCutZone !== 'number' || safeCutZone < 0 || safeCutZone > 1)
|
|
4284
|
+
throw new TypeError(
|
|
4285
|
+
`Expected 'safeCutZone' to be a number between 0 and 1, but received ${safeCutZone}`,
|
|
4286
|
+
);
|
|
4287
|
+
|
|
4288
|
+
let result = text.trim();
|
|
4289
|
+
if (result.length > limit) {
|
|
4290
|
+
// Try to cut the string into a space before the limit
|
|
4291
|
+
const safeCut = result.lastIndexOf(' ', limit);
|
|
4292
|
+
|
|
4293
|
+
if (safeCut > 0 && safeCut >= limit * safeCutZone) {
|
|
4294
|
+
// Only cuts where there is a space, and if the cut is not too early
|
|
4295
|
+
return `${result.substring(0, safeCut).trim()}...`;
|
|
4296
|
+
} else {
|
|
4297
|
+
// Emergency: Cuts straight to the limit and adds "...".
|
|
4298
|
+
return `${result.substring(0, limit).trim()}...`;
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
|
|
4302
|
+
return result;
|
|
4303
|
+
}
|
|
4304
|
+
|
|
4239
4305
|
/*
|
|
4240
4306
|
import { useEffect } from "react";
|
|
4241
4307
|
|
|
@@ -6143,9 +6209,404 @@ class TinyToastNotify {
|
|
|
6143
6209
|
|
|
6144
6210
|
/* harmony default export */ const libs_TinyToastNotify = (TinyToastNotify);
|
|
6145
6211
|
|
|
6212
|
+
;// ./src/v1/basics/collision.mjs
|
|
6213
|
+
/**
|
|
6214
|
+
* A direction relative to a rectangle.
|
|
6215
|
+
*
|
|
6216
|
+
* Represents one of the four cardinal directions from the perspective of the element.
|
|
6217
|
+
*
|
|
6218
|
+
* @typedef {'top' | 'bottom' | 'left' | 'right'} Dirs
|
|
6219
|
+
*/
|
|
6220
|
+
|
|
6221
|
+
/**
|
|
6222
|
+
* Represents all directional aspects of a collision.
|
|
6223
|
+
*
|
|
6224
|
+
* @typedef {Object} CollDirs
|
|
6225
|
+
* @property {Dirs | 'center' | null} in - The dominant direction of entry. `'center'` if all sides are equally overlapped. `null` if no collision.
|
|
6226
|
+
* @property {Dirs | null} x - The horizontal direction (`'left'` or `'right'`) the collision is biased toward, or `null`.
|
|
6227
|
+
* @property {Dirs | null} y - The vertical direction (`'top'` or `'bottom'`) the collision is biased toward, or `null`.
|
|
6228
|
+
*/
|
|
6229
|
+
|
|
6230
|
+
/**
|
|
6231
|
+
* Indicates if a collision is in the negative direction (rect2 is outside rect1).
|
|
6232
|
+
*
|
|
6233
|
+
* @typedef {Object} NegCollDirs
|
|
6234
|
+
* @property {Dirs | null} x - Horizontal direction of negative overlap, if any.
|
|
6235
|
+
* @property {Dirs | null} y - Vertical direction of negative overlap, if any.
|
|
6236
|
+
*/
|
|
6237
|
+
|
|
6238
|
+
/**
|
|
6239
|
+
* Collision depth values from each side of rect2 inside rect1.
|
|
6240
|
+
*
|
|
6241
|
+
* Positive values indicate penetration; negative values indicate gaps.
|
|
6242
|
+
*
|
|
6243
|
+
* @typedef {Object} CollData
|
|
6244
|
+
* @property {number} top - Depth from rect2's top into rect1.
|
|
6245
|
+
* @property {number} bottom - Depth from rect2's bottom into rect1.
|
|
6246
|
+
* @property {number} left - Depth from rect2's left into rect1.
|
|
6247
|
+
* @property {number} right - Depth from rect2's right into rect1.
|
|
6248
|
+
*/
|
|
6249
|
+
|
|
6250
|
+
/**
|
|
6251
|
+
* X and Y offset representing center difference between two rectangles.
|
|
6252
|
+
*
|
|
6253
|
+
* Useful to measure how far one element's center is from another.
|
|
6254
|
+
*
|
|
6255
|
+
* @typedef {Object} CollCenter
|
|
6256
|
+
* @property {number} x - Horizontal distance in pixels from rect1's center to rect2's center.
|
|
6257
|
+
* @property {number} y - Vertical distance in pixels from rect1's center to rect2's center.
|
|
6258
|
+
*/
|
|
6259
|
+
|
|
6260
|
+
/**
|
|
6261
|
+
* Represents a rectangular area in absolute pixel values.
|
|
6262
|
+
*
|
|
6263
|
+
* Similar to `DOMRect`, this object describes the dimensions and position of a box
|
|
6264
|
+
* in the 2D plane, typically representing an element's bounding box.
|
|
6265
|
+
*
|
|
6266
|
+
* @typedef {Object} ObjRect
|
|
6267
|
+
* @property {number} height - The total height of the rectangle in pixels.
|
|
6268
|
+
* @property {number} width - The total width of the rectangle in pixels.
|
|
6269
|
+
* @property {number} top - The Y-coordinate of the top edge of the rectangle.
|
|
6270
|
+
* @property {number} bottom - The Y-coordinate of the bottom edge of the rectangle.
|
|
6271
|
+
* @property {number} left - The X-coordinate of the left edge of the rectangle.
|
|
6272
|
+
* @property {number} right - The X-coordinate of the right edge of the rectangle.
|
|
6273
|
+
*/
|
|
6274
|
+
|
|
6275
|
+
// Normal collision checks (loose overlap detection)
|
|
6276
|
+
|
|
6277
|
+
/**
|
|
6278
|
+
* Checks if rect1 is completely above rect2 (no vertical overlap).
|
|
6279
|
+
*
|
|
6280
|
+
* @param {ObjRect} rect1 - The bounding rectangle of the first element.
|
|
6281
|
+
* @param {ObjRect} rect2 - The bounding rectangle of the second element.
|
|
6282
|
+
* @returns {boolean} True if rect1 is entirely above rect2.
|
|
6283
|
+
*/
|
|
6284
|
+
const collision_areElsCollTop = (rect1, rect2) => rect1.bottom < rect2.top;
|
|
6285
|
+
|
|
6286
|
+
/**
|
|
6287
|
+
* Checks if rect1 is completely below rect2 (no vertical overlap).
|
|
6288
|
+
*
|
|
6289
|
+
* @param {ObjRect} rect1
|
|
6290
|
+
* @param {ObjRect} rect2
|
|
6291
|
+
* @returns {boolean} True if rect1 is entirely below rect2.
|
|
6292
|
+
*/
|
|
6293
|
+
const collision_areElsCollBottom = (rect1, rect2) => rect1.top > rect2.bottom;
|
|
6294
|
+
|
|
6295
|
+
/**
|
|
6296
|
+
* Checks if rect1 is completely to the left of rect2 (no horizontal overlap).
|
|
6297
|
+
*
|
|
6298
|
+
* @param {ObjRect} rect1
|
|
6299
|
+
* @param {ObjRect} rect2
|
|
6300
|
+
* @returns {boolean} True if rect1 is entirely to the left of rect2.
|
|
6301
|
+
*/
|
|
6302
|
+
const collision_areElsCollLeft = (rect1, rect2) => rect1.right < rect2.left;
|
|
6303
|
+
|
|
6304
|
+
/**
|
|
6305
|
+
* Checks if rect1 is completely to the right of rect2 (no horizontal overlap).
|
|
6306
|
+
*
|
|
6307
|
+
* @param {ObjRect} rect1
|
|
6308
|
+
* @param {ObjRect} rect2
|
|
6309
|
+
* @returns {boolean} True if rect1 is entirely to the right of rect2.
|
|
6310
|
+
*/
|
|
6311
|
+
const collision_areElsCollRight = (rect1, rect2) => rect1.left > rect2.right;
|
|
6312
|
+
|
|
6313
|
+
// Perfect collision checks (touch included)
|
|
6314
|
+
|
|
6315
|
+
/**
|
|
6316
|
+
* Checks if rect1 is perfectly above rect2 (no vertical touch or overlap).
|
|
6317
|
+
*
|
|
6318
|
+
* @param {ObjRect} rect1
|
|
6319
|
+
* @param {ObjRect} rect2
|
|
6320
|
+
* @returns {boolean} True if rect1 is fully above or touching rect2's top.
|
|
6321
|
+
*/
|
|
6322
|
+
const areElsCollPerfTop = (rect1, rect2) => rect1.bottom <= rect2.top;
|
|
6323
|
+
|
|
6324
|
+
/**
|
|
6325
|
+
* Checks if rect1 is perfectly below rect2 (no vertical touch or overlap).
|
|
6326
|
+
*
|
|
6327
|
+
* @param {ObjRect} rect1
|
|
6328
|
+
* @param {ObjRect} rect2
|
|
6329
|
+
* @returns {boolean} True if rect1 is fully below or touching rect2's bottom.
|
|
6330
|
+
*/
|
|
6331
|
+
const areElsCollPerfBottom = (rect1, rect2) => rect1.top >= rect2.bottom;
|
|
6332
|
+
|
|
6333
|
+
/**
|
|
6334
|
+
* Checks if rect1 is perfectly to the left of rect2 (no horizontal touch or overlap).
|
|
6335
|
+
*
|
|
6336
|
+
* @param {ObjRect} rect1
|
|
6337
|
+
* @param {ObjRect} rect2
|
|
6338
|
+
* @returns {boolean} True if rect1 is fully left or touching rect2's left.
|
|
6339
|
+
*/
|
|
6340
|
+
const areElsCollPerfLeft = (rect1, rect2) => rect1.right <= rect2.left;
|
|
6341
|
+
|
|
6342
|
+
/**
|
|
6343
|
+
* Checks if rect1 is perfectly to the right of rect2 (no horizontal touch or overlap).
|
|
6344
|
+
*
|
|
6345
|
+
* @param {ObjRect} rect1
|
|
6346
|
+
* @param {ObjRect} rect2
|
|
6347
|
+
* @returns {boolean} True if rect1 is fully right or touching rect2's right.
|
|
6348
|
+
*/
|
|
6349
|
+
const areElsCollPerfRight = (rect1, rect2) => rect1.left >= rect2.right;
|
|
6350
|
+
|
|
6351
|
+
// Main collision check
|
|
6352
|
+
|
|
6353
|
+
/**
|
|
6354
|
+
* Returns true if rect1 and rect2 are colliding (partially or fully overlapping).
|
|
6355
|
+
*
|
|
6356
|
+
* @param {ObjRect} rect1
|
|
6357
|
+
* @param {ObjRect} rect2
|
|
6358
|
+
* @returns {boolean} True if there's any collision between rect1 and rect2.
|
|
6359
|
+
*/
|
|
6360
|
+
const collision_areElsColliding = (rect1, rect2) =>
|
|
6361
|
+
!(
|
|
6362
|
+
collision_areElsCollLeft(rect1, rect2) ||
|
|
6363
|
+
collision_areElsCollRight(rect1, rect2) ||
|
|
6364
|
+
collision_areElsCollTop(rect1, rect2) ||
|
|
6365
|
+
collision_areElsCollBottom(rect1, rect2)
|
|
6366
|
+
);
|
|
6367
|
+
|
|
6368
|
+
/**
|
|
6369
|
+
* Returns true if rect1 and rect2 are colliding or perfectly touching.
|
|
6370
|
+
*
|
|
6371
|
+
* @param {ObjRect} rect1
|
|
6372
|
+
* @param {ObjRect} rect2
|
|
6373
|
+
* @returns {boolean} True if there's any contact or overlap.
|
|
6374
|
+
*/
|
|
6375
|
+
const areElsPerfColliding = (rect1, rect2) =>
|
|
6376
|
+
!(
|
|
6377
|
+
areElsCollPerfLeft(rect1, rect2) ||
|
|
6378
|
+
areElsCollPerfRight(rect1, rect2) ||
|
|
6379
|
+
areElsCollPerfTop(rect1, rect2) ||
|
|
6380
|
+
areElsCollPerfBottom(rect1, rect2)
|
|
6381
|
+
);
|
|
6382
|
+
|
|
6383
|
+
// Collision direction guess (loose and perfect)
|
|
6384
|
+
|
|
6385
|
+
/**
|
|
6386
|
+
* Attempts to determine the direction rect1 entered rect2 based on loose overlap rules.
|
|
6387
|
+
*
|
|
6388
|
+
* @param {ObjRect} rect1
|
|
6389
|
+
* @param {ObjRect} rect2
|
|
6390
|
+
* @returns {string|null} 'top' | 'bottom' | 'left' | 'right' | null
|
|
6391
|
+
*/
|
|
6392
|
+
const getElsColliding = (rect1, rect2) => {
|
|
6393
|
+
if (collision_areElsCollLeft(rect1, rect2)) return 'left';
|
|
6394
|
+
else if (collision_areElsCollRight(rect1, rect2)) return 'right';
|
|
6395
|
+
else if (collision_areElsCollTop(rect1, rect2)) return 'top';
|
|
6396
|
+
else if (collision_areElsCollBottom(rect1, rect2)) return 'bottom';
|
|
6397
|
+
return null;
|
|
6398
|
+
};
|
|
6399
|
+
|
|
6400
|
+
/**
|
|
6401
|
+
* Attempts to determine the direction rect1 touched or entered rect2 using perfect mode.
|
|
6402
|
+
*
|
|
6403
|
+
* @param {ObjRect} rect1
|
|
6404
|
+
* @param {ObjRect} rect2
|
|
6405
|
+
* @returns {'top' | 'bottom' | 'left' | 'right' | null}
|
|
6406
|
+
*/
|
|
6407
|
+
const getElsPerfColliding = (rect1, rect2) => {
|
|
6408
|
+
if (areElsCollPerfLeft(rect1, rect2)) return 'left';
|
|
6409
|
+
else if (areElsCollPerfRight(rect1, rect2)) return 'right';
|
|
6410
|
+
else if (areElsCollPerfTop(rect1, rect2)) return 'top';
|
|
6411
|
+
else if (areElsCollPerfBottom(rect1, rect2)) return 'bottom';
|
|
6412
|
+
return null;
|
|
6413
|
+
};
|
|
6414
|
+
|
|
6415
|
+
// Overlap Calculation
|
|
6416
|
+
|
|
6417
|
+
/**
|
|
6418
|
+
* Calculates overlap values between rect1 and rect2 in all directions.
|
|
6419
|
+
*
|
|
6420
|
+
* @param {ObjRect} rect1
|
|
6421
|
+
* @param {ObjRect} rect2
|
|
6422
|
+
* @returns {{
|
|
6423
|
+
* overlapLeft: number,
|
|
6424
|
+
* overlapRight: number,
|
|
6425
|
+
* overlapTop: number,
|
|
6426
|
+
* overlapBottom: number
|
|
6427
|
+
* }} Distance of overlap from each direction (can be negative).
|
|
6428
|
+
*/
|
|
6429
|
+
const getElsCollOverlap = (rect1, rect2) => ({
|
|
6430
|
+
overlapLeft: rect2.right - rect1.left,
|
|
6431
|
+
overlapRight: rect1.right - rect2.left,
|
|
6432
|
+
overlapTop: rect2.bottom - rect1.top,
|
|
6433
|
+
overlapBottom: rect1.bottom - rect2.top,
|
|
6434
|
+
});
|
|
6435
|
+
|
|
6436
|
+
/**
|
|
6437
|
+
* Determines directional collision based on overlap depth.
|
|
6438
|
+
*
|
|
6439
|
+
* @param {Object} [settings={}]
|
|
6440
|
+
* @param {number} [settings.overlapLeft]
|
|
6441
|
+
* @param {number} [settings.overlapRight]
|
|
6442
|
+
* @param {number} [settings.overlapTop]
|
|
6443
|
+
* @param {number} [settings.overlapBottom]
|
|
6444
|
+
* @returns {{ dirX: Dirs, dirY: Dirs }} Direction of strongest X/Y overlap.
|
|
6445
|
+
*/
|
|
6446
|
+
const getElsCollOverlapPos = ({
|
|
6447
|
+
overlapLeft = -1,
|
|
6448
|
+
overlapRight = -1,
|
|
6449
|
+
overlapTop = -1,
|
|
6450
|
+
overlapBottom = -1,
|
|
6451
|
+
} = {}) => ({
|
|
6452
|
+
dirX: overlapLeft < overlapRight ? 'right' : 'left',
|
|
6453
|
+
dirY: overlapTop < overlapBottom ? 'bottom' : 'top',
|
|
6454
|
+
});
|
|
6455
|
+
|
|
6456
|
+
// Center utils
|
|
6457
|
+
|
|
6458
|
+
/**
|
|
6459
|
+
* Calculates the center point (X and Y) of a given Rect.
|
|
6460
|
+
*
|
|
6461
|
+
* @param {ObjRect} rect - The bounding rectangle of the element.
|
|
6462
|
+
* @returns {{ x: number, y: number }} An object with the `x` and `y` coordinates of the center.
|
|
6463
|
+
*/
|
|
6464
|
+
const getRectCenter = (rect) => ({
|
|
6465
|
+
x: rect.left + rect.width / 2,
|
|
6466
|
+
y: rect.top + rect.height / 2,
|
|
6467
|
+
});
|
|
6468
|
+
|
|
6469
|
+
/**
|
|
6470
|
+
* Calculates the offset between the center of rect2 and the center of rect1.
|
|
6471
|
+
*
|
|
6472
|
+
* The values will be 0 when rect1 is perfectly centered over rect2.
|
|
6473
|
+
*
|
|
6474
|
+
* @param {ObjRect} rect1 - The bounding rectangle of the reference element.
|
|
6475
|
+
* @param {ObjRect} rect2 - The bounding rectangle of the element being compared.
|
|
6476
|
+
* @returns {{
|
|
6477
|
+
* x: number,
|
|
6478
|
+
* y: number
|
|
6479
|
+
* }} An object with the X and Y offset in pixels from rect1's center to rect2's center.
|
|
6480
|
+
*/
|
|
6481
|
+
function getElsRelativeCenterOffset(rect1, rect2) {
|
|
6482
|
+
const center1X = rect1.left + rect1.width / 2;
|
|
6483
|
+
const center1Y = rect1.top + rect1.height / 2;
|
|
6484
|
+
|
|
6485
|
+
const center2X = rect2.left + rect2.width / 2;
|
|
6486
|
+
const center2Y = rect2.top + rect2.height / 2;
|
|
6487
|
+
|
|
6488
|
+
return {
|
|
6489
|
+
x: center2X - center1X,
|
|
6490
|
+
y: center2Y - center1Y,
|
|
6491
|
+
};
|
|
6492
|
+
}
|
|
6493
|
+
|
|
6494
|
+
// Direction & Depth detection
|
|
6495
|
+
|
|
6496
|
+
/**
|
|
6497
|
+
* Detects the direction of the dominant collision between two elements
|
|
6498
|
+
* and calculates how deep the overlap is in both x and y axes.
|
|
6499
|
+
*
|
|
6500
|
+
* @param {ObjRect} rect1 - The bounding rectangle of the first element.
|
|
6501
|
+
* @param {ObjRect} rect2 - The bounding rectangle of the second element.
|
|
6502
|
+
* @returns {{
|
|
6503
|
+
* inDir: Dirs | null;
|
|
6504
|
+
* dirX: Dirs | null;
|
|
6505
|
+
* dirY: Dirs | null;
|
|
6506
|
+
* depthX: number;
|
|
6507
|
+
* depthY: number;
|
|
6508
|
+
* }} An object containing the collision direction and how deep the overlap is.
|
|
6509
|
+
*/
|
|
6510
|
+
function getElsCollDirDepth(rect1, rect2) {
|
|
6511
|
+
if (!areElsPerfColliding(rect1, rect2))
|
|
6512
|
+
return {
|
|
6513
|
+
inDir: null,
|
|
6514
|
+
dirX: null,
|
|
6515
|
+
dirY: null,
|
|
6516
|
+
depthX: 0,
|
|
6517
|
+
depthY: 0,
|
|
6518
|
+
};
|
|
6519
|
+
|
|
6520
|
+
const { overlapLeft, overlapRight, overlapTop, overlapBottom } = getElsCollOverlap(rect1, rect2);
|
|
6521
|
+
const { dirX, dirY } = getElsCollOverlapPos({
|
|
6522
|
+
overlapLeft,
|
|
6523
|
+
overlapRight,
|
|
6524
|
+
overlapTop,
|
|
6525
|
+
overlapBottom,
|
|
6526
|
+
});
|
|
6527
|
+
const depthX = Math.min(overlapLeft, overlapRight);
|
|
6528
|
+
const depthY = Math.min(overlapTop, overlapBottom);
|
|
6529
|
+
|
|
6530
|
+
/** @type {Dirs} */
|
|
6531
|
+
let inDir;
|
|
6532
|
+
|
|
6533
|
+
if (depthX < depthY) inDir = dirX;
|
|
6534
|
+
else inDir = dirY;
|
|
6535
|
+
return { inDir, dirX, dirY, depthX, depthY };
|
|
6536
|
+
}
|
|
6537
|
+
|
|
6538
|
+
// Full detail report
|
|
6539
|
+
|
|
6540
|
+
/**
|
|
6541
|
+
* Detects the collision direction and depth between two DOMRects.
|
|
6542
|
+
*
|
|
6543
|
+
* @param {ObjRect} rect1 - The bounding rectangle of the first element.
|
|
6544
|
+
* @param {ObjRect} rect2 - The bounding rectangle of the second element.
|
|
6545
|
+
* @returns {{ depth: CollData; dirs: CollDirs; isNeg: NegCollDirs; }} Collision info or null if no collision is detected.
|
|
6546
|
+
*/
|
|
6547
|
+
function getElsCollDetails(rect1, rect2) {
|
|
6548
|
+
const isColliding = areElsPerfColliding(rect1, rect2);
|
|
6549
|
+
|
|
6550
|
+
/** @type {CollDirs} */
|
|
6551
|
+
const dirs = { in: null, x: null, y: null };
|
|
6552
|
+
|
|
6553
|
+
/** @type {NegCollDirs} */
|
|
6554
|
+
const isNeg = { y: null, x: null };
|
|
6555
|
+
|
|
6556
|
+
/** @type {Record<Dirs, number>} */
|
|
6557
|
+
const depth = { top: 0, bottom: 0, left: 0, right: 0 };
|
|
6558
|
+
|
|
6559
|
+
// Depth
|
|
6560
|
+
// Yes, it's actually reversed the values orders
|
|
6561
|
+
const { overlapLeft, overlapRight, overlapTop, overlapBottom } = getElsCollOverlap(rect2, rect1);
|
|
6562
|
+
depth.top = overlapTop;
|
|
6563
|
+
depth.bottom = overlapBottom;
|
|
6564
|
+
depth.left = overlapLeft;
|
|
6565
|
+
depth.right = overlapRight;
|
|
6566
|
+
|
|
6567
|
+
// Dirs
|
|
6568
|
+
/**
|
|
6569
|
+
* Detect the direction with the smallest positive overlap (entry point)
|
|
6570
|
+
* @type {[Dirs, number][]}
|
|
6571
|
+
*/
|
|
6572
|
+
// @ts-ignore
|
|
6573
|
+
const entries = Object.entries(depth)
|
|
6574
|
+
.filter(([, val]) => val > 0)
|
|
6575
|
+
.sort((a, b) => a[1] - b[1]);
|
|
6576
|
+
|
|
6577
|
+
// Yes, it's actually reversed the values orders here too
|
|
6578
|
+
const { dirX, dirY } = getElsCollOverlapPos({
|
|
6579
|
+
overlapLeft: overlapRight,
|
|
6580
|
+
overlapRight: overlapLeft,
|
|
6581
|
+
overlapTop: overlapBottom,
|
|
6582
|
+
overlapBottom: overlapTop,
|
|
6583
|
+
});
|
|
6584
|
+
dirs.y = dirY;
|
|
6585
|
+
dirs.x = dirX;
|
|
6586
|
+
|
|
6587
|
+
// isNeg
|
|
6588
|
+
if (depth.bottom < 0) isNeg.y = 'bottom';
|
|
6589
|
+
else if (depth.top < 0) isNeg.y = 'top';
|
|
6590
|
+
if (depth.left < 0) isNeg.x = 'left';
|
|
6591
|
+
else if (depth.right < 0) isNeg.x = 'right';
|
|
6592
|
+
|
|
6593
|
+
// Inside Dir
|
|
6594
|
+
dirs.in = isColliding
|
|
6595
|
+
? depth.top === depth.bottom && depth.bottom === depth.left && depth.left === depth.right
|
|
6596
|
+
? 'center'
|
|
6597
|
+
: entries.length
|
|
6598
|
+
? entries[0][0]
|
|
6599
|
+
: 'top'
|
|
6600
|
+
: null; // fallback in case of exact match
|
|
6601
|
+
|
|
6602
|
+
// Complete
|
|
6603
|
+
return { dirs, depth, isNeg };
|
|
6604
|
+
}
|
|
6605
|
+
|
|
6146
6606
|
;// ./src/v1/basics/html.mjs
|
|
6147
6607
|
|
|
6148
6608
|
|
|
6609
|
+
|
|
6149
6610
|
/**
|
|
6150
6611
|
* Checks if two DOM elements are colliding on the screen.
|
|
6151
6612
|
*
|
|
@@ -6156,44 +6617,157 @@ class TinyToastNotify {
|
|
|
6156
6617
|
function areHtmlElsColliding(elem1, elem2) {
|
|
6157
6618
|
const rect1 = elem1.getBoundingClientRect();
|
|
6158
6619
|
const rect2 = elem2.getBoundingClientRect();
|
|
6620
|
+
return collision_areElsColliding(rect1, rect2);
|
|
6621
|
+
}
|
|
6159
6622
|
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6623
|
+
/**
|
|
6624
|
+
* Checks if two DOM elements are colliding on the screen, and locks the collision
|
|
6625
|
+
* until the element exits through the same side it entered.
|
|
6626
|
+
*
|
|
6627
|
+
* @param {Element} elem1 - First DOM element (e.g. draggable or moving element).
|
|
6628
|
+
* @param {Element} elem2 - Second DOM element (e.g. a container or boundary element).
|
|
6629
|
+
* @param {'top'|'bottom'|'left'|'right'} lockDirection - Direction that must be respected to unlock the collision.
|
|
6630
|
+
* @param {WeakMap<Element, string>} stateMap - A shared WeakMap to track persistent entry direction per element.
|
|
6631
|
+
* @returns {boolean} True if collision is still active.
|
|
6632
|
+
*/
|
|
6633
|
+
function areHtmlElsCollidingWithLock(elem1, elem2, lockDirection, stateMap) {
|
|
6634
|
+
const rect1 = elem1.getBoundingClientRect();
|
|
6635
|
+
const rect2 = elem2.getBoundingClientRect();
|
|
6636
|
+
const isColliding = areElsColliding(rect1, rect2);
|
|
6637
|
+
|
|
6638
|
+
if (isColliding) {
|
|
6639
|
+
// Save entry direction
|
|
6640
|
+
if (!stateMap.has(elem1)) {
|
|
6641
|
+
stateMap.set(elem1, lockDirection);
|
|
6642
|
+
}
|
|
6643
|
+
return true;
|
|
6644
|
+
}
|
|
6645
|
+
|
|
6646
|
+
// Handle unlock logic
|
|
6647
|
+
if (stateMap.has(elem1)) {
|
|
6648
|
+
const lastDirection = stateMap.get(elem1);
|
|
6649
|
+
|
|
6650
|
+
switch (lastDirection) {
|
|
6651
|
+
case 'top':
|
|
6652
|
+
if (areElsCollTop(rect1, rect2)) stateMap.delete(elem1); // exited from top
|
|
6653
|
+
break;
|
|
6654
|
+
case 'bottom':
|
|
6655
|
+
if (areElsCollBottom(rect1, rect2)) stateMap.delete(elem1); // exited from bottom
|
|
6656
|
+
break;
|
|
6657
|
+
case 'left':
|
|
6658
|
+
if (areElsCollLeft(rect1, rect2)) stateMap.delete(elem1); // exited from left
|
|
6659
|
+
break;
|
|
6660
|
+
case 'right':
|
|
6661
|
+
if (areElsCollRight(rect1, rect2)) stateMap.delete(elem1); // exited from right
|
|
6662
|
+
break;
|
|
6663
|
+
}
|
|
6664
|
+
|
|
6665
|
+
return stateMap.has(elem1); // still colliding (locked)
|
|
6666
|
+
}
|
|
6667
|
+
|
|
6668
|
+
return false;
|
|
6166
6669
|
}
|
|
6167
6670
|
|
|
6168
6671
|
/**
|
|
6169
|
-
* Reads
|
|
6170
|
-
*
|
|
6171
|
-
* @param {File} file
|
|
6172
|
-
* @
|
|
6672
|
+
* Reads the contents of a file using the specified FileReader method.
|
|
6673
|
+
*
|
|
6674
|
+
* @param {File} file - The file to be read.
|
|
6675
|
+
* @param {'readAsArrayBuffer'|'readAsDataURL'|'readAsText'|'readAsBinaryString'} method -
|
|
6676
|
+
* The FileReader method to use for reading the file.
|
|
6677
|
+
* @returns {Promise<any>} - A promise that resolves with the file content, according to the chosen method.
|
|
6678
|
+
* @throws {Error} - If an unexpected error occurs while handling the result.
|
|
6679
|
+
* @throws {DOMException} - If the FileReader encounters an error while reading the file.
|
|
6173
6680
|
*/
|
|
6174
|
-
function
|
|
6681
|
+
function readFileBlob(file, method) {
|
|
6175
6682
|
return new Promise((resolve, reject) => {
|
|
6176
6683
|
const reader = new FileReader();
|
|
6177
|
-
|
|
6178
6684
|
reader.onload = () => {
|
|
6179
6685
|
try {
|
|
6180
|
-
|
|
6181
|
-
const result = JSON.parse(reader.result);
|
|
6182
|
-
resolve(result);
|
|
6686
|
+
resolve(reader.result);
|
|
6183
6687
|
} catch (error) {
|
|
6184
|
-
|
|
6185
|
-
reject(new Error(`Invalid JSON in file: ${file.name}\n${error.message}`));
|
|
6688
|
+
reject(error);
|
|
6186
6689
|
}
|
|
6187
6690
|
};
|
|
6188
|
-
|
|
6189
6691
|
reader.onerror = () => {
|
|
6190
|
-
reject(
|
|
6692
|
+
reject(reader.error);
|
|
6191
6693
|
};
|
|
6694
|
+
reader[method](file);
|
|
6695
|
+
});
|
|
6696
|
+
}
|
|
6192
6697
|
|
|
6193
|
-
|
|
6698
|
+
/**
|
|
6699
|
+
* Reads a file as a Base64 string using FileReader, and optionally formats it as a full data URL.
|
|
6700
|
+
*
|
|
6701
|
+
* Performs strict validation to ensure the result is a valid Base64 string or a proper data URL.
|
|
6702
|
+
*
|
|
6703
|
+
* @param {File} file - The file to be read.
|
|
6704
|
+
* @param {boolean|string} [isDataUrl=false] - If true, returns a full data URL; if false, returns only the Base64 string;
|
|
6705
|
+
* if a string is passed, it is used as the MIME type in the data URL.
|
|
6706
|
+
* @returns {Promise<string>} - A promise that resolves with the Base64 string or data URL.
|
|
6707
|
+
*
|
|
6708
|
+
* @throws {TypeError} - If the result is not a string or if `isDataUrl` is not a valid type.
|
|
6709
|
+
* @throws {Error} - If the result does not match the expected data URL format or Base64 structure.
|
|
6710
|
+
* @throws {DOMException} - If the FileReader fails to read the file.
|
|
6711
|
+
*/
|
|
6712
|
+
function readBase64Blob(file, isDataUrl = false) {
|
|
6713
|
+
return new Promise((resolve, reject) => {
|
|
6714
|
+
if (typeof isDataUrl !== 'string' && typeof isDataUrl !== 'boolean')
|
|
6715
|
+
reject(new TypeError('The isDataUrl parameter must be a boolean or a string.'));
|
|
6716
|
+
readFileBlob(file, 'readAsDataURL')
|
|
6717
|
+
.then(
|
|
6718
|
+
/**
|
|
6719
|
+
* Ensure that the URL format is correct in the required pattern
|
|
6720
|
+
* @param {string} base64Data
|
|
6721
|
+
*/ (base64Data) => {
|
|
6722
|
+
if (typeof base64Data !== 'string')
|
|
6723
|
+
throw new TypeError('Expected file content to be a string.');
|
|
6724
|
+
|
|
6725
|
+
const match = base64Data.match(/^data:(.+);base64,(.*)$/);
|
|
6726
|
+
if (!match || !match[2])
|
|
6727
|
+
throw new Error('Invalid data URL format or missing Base64 content.');
|
|
6728
|
+
const [, mimeType, base64] = match;
|
|
6729
|
+
if (!/^[\w/+]+=*$/.test(base64)) throw new Error('Base64 content is malformed.');
|
|
6730
|
+
|
|
6731
|
+
if (typeof isDataUrl === 'boolean') return resolve(isDataUrl ? base64Data : base64);
|
|
6732
|
+
if (!/^[\w-]+\/[\w.+-]+$/.test(isDataUrl))
|
|
6733
|
+
throw new Error(`Invalid MIME type string: ${isDataUrl}`);
|
|
6734
|
+
|
|
6735
|
+
return resolve(`data:${isDataUrl};base64,${base64}`);
|
|
6736
|
+
},
|
|
6737
|
+
)
|
|
6738
|
+
.catch(reject);
|
|
6194
6739
|
});
|
|
6195
6740
|
}
|
|
6196
6741
|
|
|
6742
|
+
/**
|
|
6743
|
+
* Reads a file and strictly validates its content as proper JSON using FileReader.
|
|
6744
|
+
*
|
|
6745
|
+
* Performs several checks to ensure the file contains valid, parsable JSON data.
|
|
6746
|
+
*
|
|
6747
|
+
* @param {File} file - The file to be read. It must contain valid JSON as plain text.
|
|
6748
|
+
* @returns {Promise<Record<string|number|symbol, any>|any[]>} - A promise that resolves with the parsed JSON object.
|
|
6749
|
+
*
|
|
6750
|
+
* @throws {SyntaxError} - If the file content is not valid JSON syntax.
|
|
6751
|
+
* @throws {TypeError} - If the result is not a string or does not represent a JSON value.
|
|
6752
|
+
* @throws {Error} - If the result is empty or structurally invalid as JSON.
|
|
6753
|
+
* @throws {DOMException} - If the FileReader fails to read the file.
|
|
6754
|
+
*/
|
|
6755
|
+
function readJsonBlob(file) {
|
|
6756
|
+
return new Promise((resolve, reject) =>
|
|
6757
|
+
readFileBlob(file, 'readAsText')
|
|
6758
|
+
.then((data) => {
|
|
6759
|
+
if (typeof data !== 'string') throw new TypeError('Expected file content to be a string.');
|
|
6760
|
+
const trimmed = data.trim();
|
|
6761
|
+
if (trimmed.length === 0) throw new Error('File is empty or contains only whitespace.');
|
|
6762
|
+
const parsed = JSON.parse(trimmed);
|
|
6763
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
6764
|
+
throw new Error('Parsed content is not a valid JSON object or array.');
|
|
6765
|
+
resolve(parsed);
|
|
6766
|
+
})
|
|
6767
|
+
.catch(reject),
|
|
6768
|
+
);
|
|
6769
|
+
}
|
|
6770
|
+
|
|
6197
6771
|
/**
|
|
6198
6772
|
* Saves a JSON object as a downloadable file.
|
|
6199
6773
|
* @param {string} filename
|
|
@@ -6296,7 +6870,8 @@ async function fetchJson(
|
|
|
6296
6870
|
|
|
6297
6871
|
const data = await response.json();
|
|
6298
6872
|
|
|
6299
|
-
if (!
|
|
6873
|
+
if (!Array.isArray(data) && !isJsonObject(data))
|
|
6874
|
+
throw new Error('Received invalid data instead of valid JSON.');
|
|
6300
6875
|
|
|
6301
6876
|
return data;
|
|
6302
6877
|
} catch (err) {
|
|
@@ -6398,19 +6973,34 @@ const getHtmlElPadding = (el) => {
|
|
|
6398
6973
|
|
|
6399
6974
|
/**
|
|
6400
6975
|
* Installs a script that toggles CSS classes on a given element
|
|
6401
|
-
* based on the page's visibility or focus state
|
|
6976
|
+
* based on the page's visibility or focus state, and optionally
|
|
6977
|
+
* triggers callbacks on visibility changes.
|
|
6402
6978
|
*
|
|
6403
6979
|
* @param {Object} [settings={}]
|
|
6404
6980
|
* @param {HTMLElement} [settings.element=document.body] - The element to receive visibility classes.
|
|
6405
6981
|
* @param {string} [settings.hiddenClass='windowHidden'] - CSS class applied when the page is hidden.
|
|
6406
6982
|
* @param {string} [settings.visibleClass='windowVisible'] - CSS class applied when the page is visible.
|
|
6983
|
+
* @param {() => void} [settings.onVisible] - Callback called when page becomes visible.
|
|
6984
|
+
* @param {() => void} [settings.onHidden] - Callback called when page becomes hidden.
|
|
6407
6985
|
* @returns {() => void} Function that removes all installed event listeners.
|
|
6986
|
+
* @throws {TypeError} If any provided setting is invalid.
|
|
6408
6987
|
*/
|
|
6409
6988
|
function installWindowHiddenScript({
|
|
6410
6989
|
element = document.body,
|
|
6411
6990
|
hiddenClass = 'windowHidden',
|
|
6412
6991
|
visibleClass = 'windowVisible',
|
|
6992
|
+
onVisible,
|
|
6993
|
+
onHidden,
|
|
6413
6994
|
} = {}) {
|
|
6995
|
+
if (!(element instanceof HTMLElement))
|
|
6996
|
+
throw new TypeError(`"element" must be an instance of HTMLElement.`);
|
|
6997
|
+
if (typeof hiddenClass !== 'string') throw new TypeError(`"hiddenClass" must be a string.`);
|
|
6998
|
+
if (typeof visibleClass !== 'string') throw new TypeError(`"visibleClass" must be a string.`);
|
|
6999
|
+
if (onVisible !== undefined && typeof onVisible !== 'function')
|
|
7000
|
+
throw new TypeError(`"onVisible" must be a function if provided.`);
|
|
7001
|
+
if (onHidden !== undefined && typeof onHidden !== 'function')
|
|
7002
|
+
throw new TypeError(`"onHidden" must be a function if provided.`);
|
|
7003
|
+
|
|
6414
7004
|
const removeClass = () => {
|
|
6415
7005
|
element.classList.remove(hiddenClass);
|
|
6416
7006
|
element.classList.remove(visibleClass);
|
|
@@ -6418,8 +7008,6 @@ function installWindowHiddenScript({
|
|
|
6418
7008
|
|
|
6419
7009
|
/** @type {string|null} */
|
|
6420
7010
|
let hiddenProp = null;
|
|
6421
|
-
/** @type {(this: any, evt: Event) => void} */
|
|
6422
|
-
let handler;
|
|
6423
7011
|
|
|
6424
7012
|
const visibilityEvents = [
|
|
6425
7013
|
'visibilitychange',
|
|
@@ -6437,7 +7025,8 @@ function installWindowHiddenScript({
|
|
|
6437
7025
|
}
|
|
6438
7026
|
}
|
|
6439
7027
|
|
|
6440
|
-
|
|
7028
|
+
/** @type {(this: any, evt: Event) => void} */
|
|
7029
|
+
const handler = function (evt) {
|
|
6441
7030
|
removeClass();
|
|
6442
7031
|
|
|
6443
7032
|
const type = evt?.type;
|
|
@@ -6449,10 +7038,18 @@ function installWindowHiddenScript({
|
|
|
6449
7038
|
|
|
6450
7039
|
if (visibleEvents.includes(type)) {
|
|
6451
7040
|
element.classList.add(visibleClass);
|
|
7041
|
+
onVisible?.();
|
|
6452
7042
|
} else if (hiddenEvents.includes(type)) {
|
|
6453
7043
|
element.classList.add(hiddenClass);
|
|
7044
|
+
onHidden?.();
|
|
6454
7045
|
} else {
|
|
6455
|
-
|
|
7046
|
+
if (isHidden) {
|
|
7047
|
+
element.classList.add(hiddenClass);
|
|
7048
|
+
onHidden?.();
|
|
7049
|
+
} else {
|
|
7050
|
+
element.classList.add(visibleClass);
|
|
7051
|
+
onVisible?.();
|
|
7052
|
+
}
|
|
6456
7053
|
}
|
|
6457
7054
|
};
|
|
6458
7055
|
|
|
@@ -6489,6 +7086,7 @@ function installWindowHiddenScript({
|
|
|
6489
7086
|
};
|
|
6490
7087
|
}
|
|
6491
7088
|
|
|
7089
|
+
// Trigger initial state
|
|
6492
7090
|
// @ts-ignore
|
|
6493
7091
|
const simulatedEvent = new Event(hiddenProp && document[hiddenProp] ? 'blur' : 'focus');
|
|
6494
7092
|
handler(simulatedEvent);
|
|
@@ -6496,6 +7094,38 @@ function installWindowHiddenScript({
|
|
|
6496
7094
|
return uninstall;
|
|
6497
7095
|
}
|
|
6498
7096
|
|
|
7097
|
+
/**
|
|
7098
|
+
* Checks if the given element is at least partially visible in the viewport.
|
|
7099
|
+
*
|
|
7100
|
+
* @param {HTMLElement} element - The DOM element to check.
|
|
7101
|
+
* @returns {boolean} True if the element is partially in the viewport, false otherwise.
|
|
7102
|
+
*/
|
|
7103
|
+
function isInViewport(element) {
|
|
7104
|
+
const elementTop = element.offsetTop;
|
|
7105
|
+
const elementBottom = elementTop + element.offsetHeight;
|
|
7106
|
+
|
|
7107
|
+
const viewportTop = window.scrollY;
|
|
7108
|
+
const viewportBottom = viewportTop + window.innerHeight;
|
|
7109
|
+
|
|
7110
|
+
return elementBottom > viewportTop && elementTop < viewportBottom;
|
|
7111
|
+
}
|
|
7112
|
+
|
|
7113
|
+
/**
|
|
7114
|
+
* Checks if the given element is fully visible in the viewport (top and bottom).
|
|
7115
|
+
*
|
|
7116
|
+
* @param {HTMLElement} element - The DOM element to check.
|
|
7117
|
+
* @returns {boolean} True if the element is fully visible in the viewport, false otherwise.
|
|
7118
|
+
*/
|
|
7119
|
+
function isScrolledIntoView(element) {
|
|
7120
|
+
const viewportTop = window.scrollY;
|
|
7121
|
+
const viewportBottom = viewportTop + window.innerHeight;
|
|
7122
|
+
|
|
7123
|
+
const elemTop = element.offsetTop;
|
|
7124
|
+
const elemBottom = elemTop + element.offsetHeight;
|
|
7125
|
+
|
|
7126
|
+
return elemBottom <= viewportBottom && elemTop >= viewportTop;
|
|
7127
|
+
}
|
|
7128
|
+
|
|
6499
7129
|
;// ./src/v1/libs/TinyDragDropDetector.mjs
|
|
6500
7130
|
/**
|
|
6501
7131
|
* @typedef {Object} DragAndDropOptions
|
|
@@ -8514,6 +9144,244 @@ class TinyDomReadyManager {
|
|
|
8514
9144
|
|
|
8515
9145
|
/* harmony default export */ const libs_TinyDomReadyManager = (TinyDomReadyManager);
|
|
8516
9146
|
|
|
9147
|
+
;// ./src/v1/libs/TinyNotifications.mjs
|
|
9148
|
+
|
|
9149
|
+
|
|
9150
|
+
/**
|
|
9151
|
+
* A utility class to manage browser notifications with sound and custom behavior.
|
|
9152
|
+
* Useful for triggering system notifications with optional sound, avatar icon, body truncation, and click actions.
|
|
9153
|
+
*
|
|
9154
|
+
* @class
|
|
9155
|
+
*/
|
|
9156
|
+
class TinyNotifications {
|
|
9157
|
+
/** @type {boolean} Whether notifications are currently allowed by the user. */
|
|
9158
|
+
#allowed = false;
|
|
9159
|
+
|
|
9160
|
+
/** @type {boolean} Indicates whether the user has already requested permission at least once. */
|
|
9161
|
+
#permissionRequested = false;
|
|
9162
|
+
|
|
9163
|
+
/** @type {HTMLAudioElement|null} Audio element to play when a notification is triggered. */
|
|
9164
|
+
#audio = null;
|
|
9165
|
+
|
|
9166
|
+
/** @type {number} Maximum number of characters in the notification body. */
|
|
9167
|
+
#bodyLimit = 100;
|
|
9168
|
+
|
|
9169
|
+
/** @type {string|null} Default avatar icon URL for notifications. */
|
|
9170
|
+
#defaultIcon = null;
|
|
9171
|
+
|
|
9172
|
+
/** @type {(this: Notification, evt: Event) => any} Default handler when a notification is clicked. */
|
|
9173
|
+
#defaultOnClick;
|
|
9174
|
+
|
|
9175
|
+
/**
|
|
9176
|
+
* Constructs a new instance of TinyNotifications.
|
|
9177
|
+
*
|
|
9178
|
+
* @param {Object} [settings={}] - Optional settings to initialize the notification manager.
|
|
9179
|
+
* @param {string|HTMLAudioElement|null} [settings.audio] - Path or URL to the audio file for notification sounds.
|
|
9180
|
+
* @param {string|null} [settings.defaultIcon] - Default icon URL to be used in notifications.
|
|
9181
|
+
* @param {number} [settings.bodyLimit=100] - Maximum number of characters allowed in the notification body.
|
|
9182
|
+
* @param {(this: Notification, evt: Event) => any} [settings.defaultOnClick] - Default function to execute when a notification is clicked.
|
|
9183
|
+
* @throws {TypeError} If any of the parameters are of an invalid type.
|
|
9184
|
+
*/
|
|
9185
|
+
constructor({
|
|
9186
|
+
audio = null,
|
|
9187
|
+
defaultIcon = null,
|
|
9188
|
+
bodyLimit = 100,
|
|
9189
|
+
defaultOnClick = function (event) {
|
|
9190
|
+
event.preventDefault();
|
|
9191
|
+
if (window.focus) window.focus();
|
|
9192
|
+
this.close();
|
|
9193
|
+
},
|
|
9194
|
+
} = {}) {
|
|
9195
|
+
if (!(audio instanceof HTMLAudioElement) && typeof audio !== 'string' && audio !== null)
|
|
9196
|
+
throw new TypeError('audio must be an instance of HTMLAudioElement or null.');
|
|
9197
|
+
if (defaultIcon !== null && typeof defaultIcon !== 'string')
|
|
9198
|
+
throw new TypeError('defaultIcon must be a string or null.');
|
|
9199
|
+
if (!Number.isFinite(bodyLimit) || bodyLimit < 0)
|
|
9200
|
+
throw new TypeError('bodyLimit must be a non-negative number.');
|
|
9201
|
+
if (typeof defaultOnClick !== 'function')
|
|
9202
|
+
throw new TypeError('defaultOnClick must be a function.');
|
|
9203
|
+
|
|
9204
|
+
this.#audio = typeof audio !== 'string' ? audio : new Audio(audio);
|
|
9205
|
+
this.#defaultIcon = defaultIcon;
|
|
9206
|
+
this.#bodyLimit = bodyLimit;
|
|
9207
|
+
this.#defaultOnClick = defaultOnClick;
|
|
9208
|
+
}
|
|
9209
|
+
|
|
9210
|
+
/**
|
|
9211
|
+
* Requests permission from the user to show notifications.
|
|
9212
|
+
* Updates the internal `#allowed` flag.
|
|
9213
|
+
*
|
|
9214
|
+
* @returns {Promise<boolean>} Resolves to `true` if permission is granted, otherwise `false`.
|
|
9215
|
+
*/
|
|
9216
|
+
requestPerm() {
|
|
9217
|
+
const tinyThis = this;
|
|
9218
|
+
return new Promise((resolve, reject) => {
|
|
9219
|
+
if (tinyThis.isCompatible()) {
|
|
9220
|
+
if (Notification.permission === 'default') {
|
|
9221
|
+
Notification.requestPermission()
|
|
9222
|
+
.then((permission) => {
|
|
9223
|
+
this.#permissionRequested = true;
|
|
9224
|
+
tinyThis.#allowed = permission === 'granted';
|
|
9225
|
+
resolve(tinyThis.#allowed);
|
|
9226
|
+
})
|
|
9227
|
+
.catch(reject);
|
|
9228
|
+
} else {
|
|
9229
|
+
this.#permissionRequested = true;
|
|
9230
|
+
tinyThis.#allowed = Notification.permission === 'granted';
|
|
9231
|
+
resolve(tinyThis.#allowed);
|
|
9232
|
+
}
|
|
9233
|
+
} else {
|
|
9234
|
+
this.#permissionRequested = true;
|
|
9235
|
+
tinyThis.#allowed = false;
|
|
9236
|
+
resolve(false);
|
|
9237
|
+
}
|
|
9238
|
+
});
|
|
9239
|
+
}
|
|
9240
|
+
|
|
9241
|
+
/**
|
|
9242
|
+
* Checks if the Notification API is supported by the current browser.
|
|
9243
|
+
*
|
|
9244
|
+
* @returns {boolean} Returns `true` if notifications are supported, otherwise `false`.
|
|
9245
|
+
*/
|
|
9246
|
+
isCompatible() {
|
|
9247
|
+
return 'Notification' in window;
|
|
9248
|
+
}
|
|
9249
|
+
|
|
9250
|
+
/**
|
|
9251
|
+
* Sends a browser notification with the provided title and configuration.
|
|
9252
|
+
* Truncates the body if necessary and plays a sound if configured.
|
|
9253
|
+
*
|
|
9254
|
+
* @param {string} title - The title of the notification.
|
|
9255
|
+
* @param {NotificationOptions & { vibrate?: number[] }} [config={}] - Optional configuration for the notification.
|
|
9256
|
+
* @returns {Notification|null} The created `Notification` instance, or `null` if permission is not granted.
|
|
9257
|
+
* @throws {TypeError} If the title is not a string or config is not a valid object.
|
|
9258
|
+
*/
|
|
9259
|
+
send(title, config = {}) {
|
|
9260
|
+
if (!this.#permissionRequested)
|
|
9261
|
+
throw new Error('You must call requestPerm() before sending a notification.');
|
|
9262
|
+
if (typeof title !== 'string') throw new TypeError('title must be a string.');
|
|
9263
|
+
if (typeof config !== 'object' || config === null)
|
|
9264
|
+
throw new TypeError('config must be a non-null object.');
|
|
9265
|
+
|
|
9266
|
+
if (!this.#allowed) return null;
|
|
9267
|
+
|
|
9268
|
+
const { icon = this.#defaultIcon || undefined, vibrate = [200, 100, 200] } = config;
|
|
9269
|
+
const options = { ...config };
|
|
9270
|
+
if (typeof icon === 'string') options.icon = icon;
|
|
9271
|
+
if (Array.isArray(vibrate)) options.vibrate = vibrate;
|
|
9272
|
+
|
|
9273
|
+
if (typeof options.body === 'string')
|
|
9274
|
+
options.body = safeTextTrim(options.body, this.#bodyLimit);
|
|
9275
|
+
|
|
9276
|
+
const notification = new Notification(title, options);
|
|
9277
|
+
notification.addEventListener('show', () => {
|
|
9278
|
+
if (!(this.#audio instanceof HTMLAudioElement)) return;
|
|
9279
|
+
this.#audio.currentTime = 0;
|
|
9280
|
+
this.#audio.play().catch((err) => console.error(err));
|
|
9281
|
+
});
|
|
9282
|
+
|
|
9283
|
+
if (typeof this.#defaultOnClick === 'function')
|
|
9284
|
+
notification.addEventListener('click', this.#defaultOnClick);
|
|
9285
|
+
|
|
9286
|
+
return notification;
|
|
9287
|
+
}
|
|
9288
|
+
|
|
9289
|
+
// === Getters and Setters ===
|
|
9290
|
+
|
|
9291
|
+
/**
|
|
9292
|
+
* Whether the requestPerm() method was already called.
|
|
9293
|
+
* @returns {boolean}
|
|
9294
|
+
*/
|
|
9295
|
+
wasPermissionRequested() {
|
|
9296
|
+
return this.#permissionRequested;
|
|
9297
|
+
}
|
|
9298
|
+
|
|
9299
|
+
/**
|
|
9300
|
+
* Returns the current permission status.
|
|
9301
|
+
* @returns {boolean} `true` if permission was granted, otherwise `false`.
|
|
9302
|
+
*/
|
|
9303
|
+
isAllowed() {
|
|
9304
|
+
return this.#allowed;
|
|
9305
|
+
}
|
|
9306
|
+
|
|
9307
|
+
/**
|
|
9308
|
+
* Gets the current notification audio.
|
|
9309
|
+
* @returns {HTMLAudioElement|null} The sound element, or `null` if not set.
|
|
9310
|
+
*/
|
|
9311
|
+
getAudio() {
|
|
9312
|
+
return this.#audio;
|
|
9313
|
+
}
|
|
9314
|
+
|
|
9315
|
+
/**
|
|
9316
|
+
* Sets the audio element used for notification sounds.
|
|
9317
|
+
* @param {HTMLAudioElement|string|null} value - A valid `HTMLAudioElement` or `null` to disable sound.
|
|
9318
|
+
* @throws {TypeError} If the value is not an `HTMLAudioElement` or `null`.
|
|
9319
|
+
*/
|
|
9320
|
+
setAudio(value) {
|
|
9321
|
+
if (!(value instanceof HTMLAudioElement) && typeof value !== 'string' && value !== null)
|
|
9322
|
+
throw new TypeError('sound must be an instance of HTMLAudioElement or null.');
|
|
9323
|
+
this.#audio = typeof value !== 'string' ? value : new Audio(value);
|
|
9324
|
+
}
|
|
9325
|
+
|
|
9326
|
+
/**
|
|
9327
|
+
* Gets the maximum length of the notification body text.
|
|
9328
|
+
* @returns {number} Number of characters allowed.
|
|
9329
|
+
*/
|
|
9330
|
+
getBodyLimit() {
|
|
9331
|
+
return this.#bodyLimit;
|
|
9332
|
+
}
|
|
9333
|
+
|
|
9334
|
+
/**
|
|
9335
|
+
* Sets the maximum number of characters allowed in the notification body.
|
|
9336
|
+
* @param {number} value - A non-negative integer.
|
|
9337
|
+
* @throws {TypeError} If the value is not a valid non-negative number.
|
|
9338
|
+
*/
|
|
9339
|
+
setBodyLimit(value) {
|
|
9340
|
+
if (!Number.isFinite(value) || value < 0)
|
|
9341
|
+
throw new TypeError('bodyLimit must be a non-negative number.');
|
|
9342
|
+
this.#bodyLimit = value;
|
|
9343
|
+
}
|
|
9344
|
+
|
|
9345
|
+
/**
|
|
9346
|
+
* Gets the default avatar icon URL.
|
|
9347
|
+
* @returns {string|null} The URL string or `null`.
|
|
9348
|
+
*/
|
|
9349
|
+
getDefaultAvatar() {
|
|
9350
|
+
return this.#defaultIcon;
|
|
9351
|
+
}
|
|
9352
|
+
|
|
9353
|
+
/**
|
|
9354
|
+
* Sets the default avatar icon URL.
|
|
9355
|
+
* @param {string|null} value - A string URL or `null` to disable default icon.
|
|
9356
|
+
* @throws {TypeError} If the value is not a string or `null`.
|
|
9357
|
+
*/
|
|
9358
|
+
setDefaultAvatar(value) {
|
|
9359
|
+
if (!(typeof value === 'string' || value === null))
|
|
9360
|
+
throw new TypeError('defaultIcon must be a string or null.');
|
|
9361
|
+
this.#defaultIcon = value;
|
|
9362
|
+
}
|
|
9363
|
+
|
|
9364
|
+
/**
|
|
9365
|
+
* Gets the default click event handler for notifications.
|
|
9366
|
+
* @returns {(this: Notification, evt: Event) => any} The current click handler function.
|
|
9367
|
+
*/
|
|
9368
|
+
getDefaultOnClick() {
|
|
9369
|
+
return this.#defaultOnClick;
|
|
9370
|
+
}
|
|
9371
|
+
|
|
9372
|
+
/**
|
|
9373
|
+
* Sets the default click event handler for notifications.
|
|
9374
|
+
* @param {(this: Notification, evt: Event) => any} value - A function to handle the notification click event.
|
|
9375
|
+
* @throws {TypeError} If the value is not a function.
|
|
9376
|
+
*/
|
|
9377
|
+
setDefaultOnClick(value) {
|
|
9378
|
+
if (typeof value !== 'function') throw new TypeError('defaultOnClick must be a function.');
|
|
9379
|
+
this.#defaultOnClick = value;
|
|
9380
|
+
}
|
|
9381
|
+
}
|
|
9382
|
+
|
|
9383
|
+
/* harmony default export */ const libs_TinyNotifications = (TinyNotifications);
|
|
9384
|
+
|
|
8517
9385
|
;// ./src/v1/index.mjs
|
|
8518
9386
|
|
|
8519
9387
|
|
|
@@ -8538,6 +9406,8 @@ class TinyDomReadyManager {
|
|
|
8538
9406
|
|
|
8539
9407
|
|
|
8540
9408
|
|
|
9409
|
+
|
|
9410
|
+
|
|
8541
9411
|
|
|
8542
9412
|
|
|
8543
9413
|
|