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
package/dist/v1/TinyBasicsEs.js
CHANGED
|
@@ -2143,6 +2143,16 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
2143
2143
|
// EXPORTS
|
|
2144
2144
|
__webpack_require__.d(__webpack_exports__, {
|
|
2145
2145
|
addAiMarkerShortcut: () => (/* reexport */ addAiMarkerShortcut),
|
|
2146
|
+
areElsCollBottom: () => (/* reexport */ collision_areElsCollBottom),
|
|
2147
|
+
areElsCollLeft: () => (/* reexport */ collision_areElsCollLeft),
|
|
2148
|
+
areElsCollPerfBottom: () => (/* reexport */ areElsCollPerfBottom),
|
|
2149
|
+
areElsCollPerfLeft: () => (/* reexport */ areElsCollPerfLeft),
|
|
2150
|
+
areElsCollPerfRight: () => (/* reexport */ areElsCollPerfRight),
|
|
2151
|
+
areElsCollPerfTop: () => (/* reexport */ areElsCollPerfTop),
|
|
2152
|
+
areElsCollRight: () => (/* reexport */ collision_areElsCollRight),
|
|
2153
|
+
areElsCollTop: () => (/* reexport */ collision_areElsCollTop),
|
|
2154
|
+
areElsColliding: () => (/* reexport */ collision_areElsColliding),
|
|
2155
|
+
areElsPerfColliding: () => (/* reexport */ areElsPerfColliding),
|
|
2146
2156
|
areHtmlElsColliding: () => (/* reexport */ areHtmlElsColliding),
|
|
2147
2157
|
arraySortPositions: () => (/* reexport */ arraySortPositions),
|
|
2148
2158
|
asyncReplace: () => (/* reexport */ asyncReplace),
|
|
@@ -2158,23 +2168,36 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
2158
2168
|
formatTimer: () => (/* reexport */ formatTimer),
|
|
2159
2169
|
genFibonacciSeq: () => (/* reexport */ genFibonacciSeq),
|
|
2160
2170
|
getAge: () => (/* reexport */ getAge),
|
|
2171
|
+
getElsCollDetails: () => (/* reexport */ getElsCollDetails),
|
|
2172
|
+
getElsCollDirDepth: () => (/* reexport */ getElsCollDirDepth),
|
|
2173
|
+
getElsCollOverlap: () => (/* reexport */ getElsCollOverlap),
|
|
2174
|
+
getElsCollOverlapPos: () => (/* reexport */ getElsCollOverlapPos),
|
|
2175
|
+
getElsColliding: () => (/* reexport */ getElsColliding),
|
|
2176
|
+
getElsPerfColliding: () => (/* reexport */ getElsPerfColliding),
|
|
2177
|
+
getElsRelativeCenterOffset: () => (/* reexport */ getElsRelativeCenterOffset),
|
|
2161
2178
|
getHtmlElBorders: () => (/* reexport */ getHtmlElBorders),
|
|
2162
2179
|
getHtmlElBordersWidth: () => (/* reexport */ getHtmlElBordersWidth),
|
|
2163
2180
|
getHtmlElMargin: () => (/* reexport */ getHtmlElMargin),
|
|
2164
2181
|
getHtmlElPadding: () => (/* reexport */ getHtmlElPadding),
|
|
2182
|
+
getRectCenter: () => (/* reexport */ getRectCenter),
|
|
2165
2183
|
getSimplePerc: () => (/* reexport */ getSimplePerc),
|
|
2166
2184
|
getTimeDuration: () => (/* reexport */ getTimeDuration),
|
|
2167
2185
|
installWindowHiddenScript: () => (/* reexport */ installWindowHiddenScript),
|
|
2168
2186
|
isFullScreenMode: () => (/* reexport */ isFullScreenMode),
|
|
2187
|
+
isInViewport: () => (/* reexport */ isInViewport),
|
|
2169
2188
|
isJsonObject: () => (/* reexport */ isJsonObject),
|
|
2170
2189
|
isScreenFilled: () => (/* reexport */ isScreenFilled),
|
|
2190
|
+
isScrolledIntoView: () => (/* reexport */ isScrolledIntoView),
|
|
2171
2191
|
objType: () => (/* reexport */ objType),
|
|
2172
2192
|
offFullScreenChange: () => (/* reexport */ offFullScreenChange),
|
|
2173
2193
|
onFullScreenChange: () => (/* reexport */ onFullScreenChange),
|
|
2194
|
+
readBase64Blob: () => (/* reexport */ readBase64Blob),
|
|
2195
|
+
readFileBlob: () => (/* reexport */ readFileBlob),
|
|
2174
2196
|
readJsonBlob: () => (/* reexport */ readJsonBlob),
|
|
2175
2197
|
reorderObjTypeOrder: () => (/* reexport */ reorderObjTypeOrder),
|
|
2176
2198
|
requestFullScreen: () => (/* reexport */ requestFullScreen),
|
|
2177
2199
|
ruleOfThree: () => (/* reexport */ ruleOfThree),
|
|
2200
|
+
safeTextTrim: () => (/* reexport */ safeTextTrim),
|
|
2178
2201
|
saveJsonFile: () => (/* reexport */ saveJsonFile),
|
|
2179
2202
|
shuffleArray: () => (/* reexport */ shuffleArray),
|
|
2180
2203
|
toTitleCase: () => (/* reexport */ toTitleCase),
|
|
@@ -2848,9 +2871,404 @@ extendObjType([
|
|
|
2848
2871
|
],
|
|
2849
2872
|
]);
|
|
2850
2873
|
|
|
2874
|
+
;// ./src/v1/basics/collision.mjs
|
|
2875
|
+
/**
|
|
2876
|
+
* A direction relative to a rectangle.
|
|
2877
|
+
*
|
|
2878
|
+
* Represents one of the four cardinal directions from the perspective of the element.
|
|
2879
|
+
*
|
|
2880
|
+
* @typedef {'top' | 'bottom' | 'left' | 'right'} Dirs
|
|
2881
|
+
*/
|
|
2882
|
+
|
|
2883
|
+
/**
|
|
2884
|
+
* Represents all directional aspects of a collision.
|
|
2885
|
+
*
|
|
2886
|
+
* @typedef {Object} CollDirs
|
|
2887
|
+
* @property {Dirs | 'center' | null} in - The dominant direction of entry. `'center'` if all sides are equally overlapped. `null` if no collision.
|
|
2888
|
+
* @property {Dirs | null} x - The horizontal direction (`'left'` or `'right'`) the collision is biased toward, or `null`.
|
|
2889
|
+
* @property {Dirs | null} y - The vertical direction (`'top'` or `'bottom'`) the collision is biased toward, or `null`.
|
|
2890
|
+
*/
|
|
2891
|
+
|
|
2892
|
+
/**
|
|
2893
|
+
* Indicates if a collision is in the negative direction (rect2 is outside rect1).
|
|
2894
|
+
*
|
|
2895
|
+
* @typedef {Object} NegCollDirs
|
|
2896
|
+
* @property {Dirs | null} x - Horizontal direction of negative overlap, if any.
|
|
2897
|
+
* @property {Dirs | null} y - Vertical direction of negative overlap, if any.
|
|
2898
|
+
*/
|
|
2899
|
+
|
|
2900
|
+
/**
|
|
2901
|
+
* Collision depth values from each side of rect2 inside rect1.
|
|
2902
|
+
*
|
|
2903
|
+
* Positive values indicate penetration; negative values indicate gaps.
|
|
2904
|
+
*
|
|
2905
|
+
* @typedef {Object} CollData
|
|
2906
|
+
* @property {number} top - Depth from rect2's top into rect1.
|
|
2907
|
+
* @property {number} bottom - Depth from rect2's bottom into rect1.
|
|
2908
|
+
* @property {number} left - Depth from rect2's left into rect1.
|
|
2909
|
+
* @property {number} right - Depth from rect2's right into rect1.
|
|
2910
|
+
*/
|
|
2911
|
+
|
|
2912
|
+
/**
|
|
2913
|
+
* X and Y offset representing center difference between two rectangles.
|
|
2914
|
+
*
|
|
2915
|
+
* Useful to measure how far one element's center is from another.
|
|
2916
|
+
*
|
|
2917
|
+
* @typedef {Object} CollCenter
|
|
2918
|
+
* @property {number} x - Horizontal distance in pixels from rect1's center to rect2's center.
|
|
2919
|
+
* @property {number} y - Vertical distance in pixels from rect1's center to rect2's center.
|
|
2920
|
+
*/
|
|
2921
|
+
|
|
2922
|
+
/**
|
|
2923
|
+
* Represents a rectangular area in absolute pixel values.
|
|
2924
|
+
*
|
|
2925
|
+
* Similar to `DOMRect`, this object describes the dimensions and position of a box
|
|
2926
|
+
* in the 2D plane, typically representing an element's bounding box.
|
|
2927
|
+
*
|
|
2928
|
+
* @typedef {Object} ObjRect
|
|
2929
|
+
* @property {number} height - The total height of the rectangle in pixels.
|
|
2930
|
+
* @property {number} width - The total width of the rectangle in pixels.
|
|
2931
|
+
* @property {number} top - The Y-coordinate of the top edge of the rectangle.
|
|
2932
|
+
* @property {number} bottom - The Y-coordinate of the bottom edge of the rectangle.
|
|
2933
|
+
* @property {number} left - The X-coordinate of the left edge of the rectangle.
|
|
2934
|
+
* @property {number} right - The X-coordinate of the right edge of the rectangle.
|
|
2935
|
+
*/
|
|
2936
|
+
|
|
2937
|
+
// Normal collision checks (loose overlap detection)
|
|
2938
|
+
|
|
2939
|
+
/**
|
|
2940
|
+
* Checks if rect1 is completely above rect2 (no vertical overlap).
|
|
2941
|
+
*
|
|
2942
|
+
* @param {ObjRect} rect1 - The bounding rectangle of the first element.
|
|
2943
|
+
* @param {ObjRect} rect2 - The bounding rectangle of the second element.
|
|
2944
|
+
* @returns {boolean} True if rect1 is entirely above rect2.
|
|
2945
|
+
*/
|
|
2946
|
+
const collision_areElsCollTop = (rect1, rect2) => rect1.bottom < rect2.top;
|
|
2947
|
+
|
|
2948
|
+
/**
|
|
2949
|
+
* Checks if rect1 is completely below rect2 (no vertical overlap).
|
|
2950
|
+
*
|
|
2951
|
+
* @param {ObjRect} rect1
|
|
2952
|
+
* @param {ObjRect} rect2
|
|
2953
|
+
* @returns {boolean} True if rect1 is entirely below rect2.
|
|
2954
|
+
*/
|
|
2955
|
+
const collision_areElsCollBottom = (rect1, rect2) => rect1.top > rect2.bottom;
|
|
2956
|
+
|
|
2957
|
+
/**
|
|
2958
|
+
* Checks if rect1 is completely to the left of rect2 (no horizontal overlap).
|
|
2959
|
+
*
|
|
2960
|
+
* @param {ObjRect} rect1
|
|
2961
|
+
* @param {ObjRect} rect2
|
|
2962
|
+
* @returns {boolean} True if rect1 is entirely to the left of rect2.
|
|
2963
|
+
*/
|
|
2964
|
+
const collision_areElsCollLeft = (rect1, rect2) => rect1.right < rect2.left;
|
|
2965
|
+
|
|
2966
|
+
/**
|
|
2967
|
+
* Checks if rect1 is completely to the right of rect2 (no horizontal overlap).
|
|
2968
|
+
*
|
|
2969
|
+
* @param {ObjRect} rect1
|
|
2970
|
+
* @param {ObjRect} rect2
|
|
2971
|
+
* @returns {boolean} True if rect1 is entirely to the right of rect2.
|
|
2972
|
+
*/
|
|
2973
|
+
const collision_areElsCollRight = (rect1, rect2) => rect1.left > rect2.right;
|
|
2974
|
+
|
|
2975
|
+
// Perfect collision checks (touch included)
|
|
2976
|
+
|
|
2977
|
+
/**
|
|
2978
|
+
* Checks if rect1 is perfectly above rect2 (no vertical touch or overlap).
|
|
2979
|
+
*
|
|
2980
|
+
* @param {ObjRect} rect1
|
|
2981
|
+
* @param {ObjRect} rect2
|
|
2982
|
+
* @returns {boolean} True if rect1 is fully above or touching rect2's top.
|
|
2983
|
+
*/
|
|
2984
|
+
const areElsCollPerfTop = (rect1, rect2) => rect1.bottom <= rect2.top;
|
|
2985
|
+
|
|
2986
|
+
/**
|
|
2987
|
+
* Checks if rect1 is perfectly below rect2 (no vertical touch or overlap).
|
|
2988
|
+
*
|
|
2989
|
+
* @param {ObjRect} rect1
|
|
2990
|
+
* @param {ObjRect} rect2
|
|
2991
|
+
* @returns {boolean} True if rect1 is fully below or touching rect2's bottom.
|
|
2992
|
+
*/
|
|
2993
|
+
const areElsCollPerfBottom = (rect1, rect2) => rect1.top >= rect2.bottom;
|
|
2994
|
+
|
|
2995
|
+
/**
|
|
2996
|
+
* Checks if rect1 is perfectly to the left of rect2 (no horizontal touch or overlap).
|
|
2997
|
+
*
|
|
2998
|
+
* @param {ObjRect} rect1
|
|
2999
|
+
* @param {ObjRect} rect2
|
|
3000
|
+
* @returns {boolean} True if rect1 is fully left or touching rect2's left.
|
|
3001
|
+
*/
|
|
3002
|
+
const areElsCollPerfLeft = (rect1, rect2) => rect1.right <= rect2.left;
|
|
3003
|
+
|
|
3004
|
+
/**
|
|
3005
|
+
* Checks if rect1 is perfectly to the right of rect2 (no horizontal touch or overlap).
|
|
3006
|
+
*
|
|
3007
|
+
* @param {ObjRect} rect1
|
|
3008
|
+
* @param {ObjRect} rect2
|
|
3009
|
+
* @returns {boolean} True if rect1 is fully right or touching rect2's right.
|
|
3010
|
+
*/
|
|
3011
|
+
const areElsCollPerfRight = (rect1, rect2) => rect1.left >= rect2.right;
|
|
3012
|
+
|
|
3013
|
+
// Main collision check
|
|
3014
|
+
|
|
3015
|
+
/**
|
|
3016
|
+
* Returns true if rect1 and rect2 are colliding (partially or fully overlapping).
|
|
3017
|
+
*
|
|
3018
|
+
* @param {ObjRect} rect1
|
|
3019
|
+
* @param {ObjRect} rect2
|
|
3020
|
+
* @returns {boolean} True if there's any collision between rect1 and rect2.
|
|
3021
|
+
*/
|
|
3022
|
+
const collision_areElsColliding = (rect1, rect2) =>
|
|
3023
|
+
!(
|
|
3024
|
+
collision_areElsCollLeft(rect1, rect2) ||
|
|
3025
|
+
collision_areElsCollRight(rect1, rect2) ||
|
|
3026
|
+
collision_areElsCollTop(rect1, rect2) ||
|
|
3027
|
+
collision_areElsCollBottom(rect1, rect2)
|
|
3028
|
+
);
|
|
3029
|
+
|
|
3030
|
+
/**
|
|
3031
|
+
* Returns true if rect1 and rect2 are colliding or perfectly touching.
|
|
3032
|
+
*
|
|
3033
|
+
* @param {ObjRect} rect1
|
|
3034
|
+
* @param {ObjRect} rect2
|
|
3035
|
+
* @returns {boolean} True if there's any contact or overlap.
|
|
3036
|
+
*/
|
|
3037
|
+
const areElsPerfColliding = (rect1, rect2) =>
|
|
3038
|
+
!(
|
|
3039
|
+
areElsCollPerfLeft(rect1, rect2) ||
|
|
3040
|
+
areElsCollPerfRight(rect1, rect2) ||
|
|
3041
|
+
areElsCollPerfTop(rect1, rect2) ||
|
|
3042
|
+
areElsCollPerfBottom(rect1, rect2)
|
|
3043
|
+
);
|
|
3044
|
+
|
|
3045
|
+
// Collision direction guess (loose and perfect)
|
|
3046
|
+
|
|
3047
|
+
/**
|
|
3048
|
+
* Attempts to determine the direction rect1 entered rect2 based on loose overlap rules.
|
|
3049
|
+
*
|
|
3050
|
+
* @param {ObjRect} rect1
|
|
3051
|
+
* @param {ObjRect} rect2
|
|
3052
|
+
* @returns {string|null} 'top' | 'bottom' | 'left' | 'right' | null
|
|
3053
|
+
*/
|
|
3054
|
+
const getElsColliding = (rect1, rect2) => {
|
|
3055
|
+
if (collision_areElsCollLeft(rect1, rect2)) return 'left';
|
|
3056
|
+
else if (collision_areElsCollRight(rect1, rect2)) return 'right';
|
|
3057
|
+
else if (collision_areElsCollTop(rect1, rect2)) return 'top';
|
|
3058
|
+
else if (collision_areElsCollBottom(rect1, rect2)) return 'bottom';
|
|
3059
|
+
return null;
|
|
3060
|
+
};
|
|
3061
|
+
|
|
3062
|
+
/**
|
|
3063
|
+
* Attempts to determine the direction rect1 touched or entered rect2 using perfect mode.
|
|
3064
|
+
*
|
|
3065
|
+
* @param {ObjRect} rect1
|
|
3066
|
+
* @param {ObjRect} rect2
|
|
3067
|
+
* @returns {'top' | 'bottom' | 'left' | 'right' | null}
|
|
3068
|
+
*/
|
|
3069
|
+
const getElsPerfColliding = (rect1, rect2) => {
|
|
3070
|
+
if (areElsCollPerfLeft(rect1, rect2)) return 'left';
|
|
3071
|
+
else if (areElsCollPerfRight(rect1, rect2)) return 'right';
|
|
3072
|
+
else if (areElsCollPerfTop(rect1, rect2)) return 'top';
|
|
3073
|
+
else if (areElsCollPerfBottom(rect1, rect2)) return 'bottom';
|
|
3074
|
+
return null;
|
|
3075
|
+
};
|
|
3076
|
+
|
|
3077
|
+
// Overlap Calculation
|
|
3078
|
+
|
|
3079
|
+
/**
|
|
3080
|
+
* Calculates overlap values between rect1 and rect2 in all directions.
|
|
3081
|
+
*
|
|
3082
|
+
* @param {ObjRect} rect1
|
|
3083
|
+
* @param {ObjRect} rect2
|
|
3084
|
+
* @returns {{
|
|
3085
|
+
* overlapLeft: number,
|
|
3086
|
+
* overlapRight: number,
|
|
3087
|
+
* overlapTop: number,
|
|
3088
|
+
* overlapBottom: number
|
|
3089
|
+
* }} Distance of overlap from each direction (can be negative).
|
|
3090
|
+
*/
|
|
3091
|
+
const getElsCollOverlap = (rect1, rect2) => ({
|
|
3092
|
+
overlapLeft: rect2.right - rect1.left,
|
|
3093
|
+
overlapRight: rect1.right - rect2.left,
|
|
3094
|
+
overlapTop: rect2.bottom - rect1.top,
|
|
3095
|
+
overlapBottom: rect1.bottom - rect2.top,
|
|
3096
|
+
});
|
|
3097
|
+
|
|
3098
|
+
/**
|
|
3099
|
+
* Determines directional collision based on overlap depth.
|
|
3100
|
+
*
|
|
3101
|
+
* @param {Object} [settings={}]
|
|
3102
|
+
* @param {number} [settings.overlapLeft]
|
|
3103
|
+
* @param {number} [settings.overlapRight]
|
|
3104
|
+
* @param {number} [settings.overlapTop]
|
|
3105
|
+
* @param {number} [settings.overlapBottom]
|
|
3106
|
+
* @returns {{ dirX: Dirs, dirY: Dirs }} Direction of strongest X/Y overlap.
|
|
3107
|
+
*/
|
|
3108
|
+
const getElsCollOverlapPos = ({
|
|
3109
|
+
overlapLeft = -1,
|
|
3110
|
+
overlapRight = -1,
|
|
3111
|
+
overlapTop = -1,
|
|
3112
|
+
overlapBottom = -1,
|
|
3113
|
+
} = {}) => ({
|
|
3114
|
+
dirX: overlapLeft < overlapRight ? 'right' : 'left',
|
|
3115
|
+
dirY: overlapTop < overlapBottom ? 'bottom' : 'top',
|
|
3116
|
+
});
|
|
3117
|
+
|
|
3118
|
+
// Center utils
|
|
3119
|
+
|
|
3120
|
+
/**
|
|
3121
|
+
* Calculates the center point (X and Y) of a given Rect.
|
|
3122
|
+
*
|
|
3123
|
+
* @param {ObjRect} rect - The bounding rectangle of the element.
|
|
3124
|
+
* @returns {{ x: number, y: number }} An object with the `x` and `y` coordinates of the center.
|
|
3125
|
+
*/
|
|
3126
|
+
const getRectCenter = (rect) => ({
|
|
3127
|
+
x: rect.left + rect.width / 2,
|
|
3128
|
+
y: rect.top + rect.height / 2,
|
|
3129
|
+
});
|
|
3130
|
+
|
|
3131
|
+
/**
|
|
3132
|
+
* Calculates the offset between the center of rect2 and the center of rect1.
|
|
3133
|
+
*
|
|
3134
|
+
* The values will be 0 when rect1 is perfectly centered over rect2.
|
|
3135
|
+
*
|
|
3136
|
+
* @param {ObjRect} rect1 - The bounding rectangle of the reference element.
|
|
3137
|
+
* @param {ObjRect} rect2 - The bounding rectangle of the element being compared.
|
|
3138
|
+
* @returns {{
|
|
3139
|
+
* x: number,
|
|
3140
|
+
* y: number
|
|
3141
|
+
* }} An object with the X and Y offset in pixels from rect1's center to rect2's center.
|
|
3142
|
+
*/
|
|
3143
|
+
function getElsRelativeCenterOffset(rect1, rect2) {
|
|
3144
|
+
const center1X = rect1.left + rect1.width / 2;
|
|
3145
|
+
const center1Y = rect1.top + rect1.height / 2;
|
|
3146
|
+
|
|
3147
|
+
const center2X = rect2.left + rect2.width / 2;
|
|
3148
|
+
const center2Y = rect2.top + rect2.height / 2;
|
|
3149
|
+
|
|
3150
|
+
return {
|
|
3151
|
+
x: center2X - center1X,
|
|
3152
|
+
y: center2Y - center1Y,
|
|
3153
|
+
};
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
// Direction & Depth detection
|
|
3157
|
+
|
|
3158
|
+
/**
|
|
3159
|
+
* Detects the direction of the dominant collision between two elements
|
|
3160
|
+
* and calculates how deep the overlap is in both x and y axes.
|
|
3161
|
+
*
|
|
3162
|
+
* @param {ObjRect} rect1 - The bounding rectangle of the first element.
|
|
3163
|
+
* @param {ObjRect} rect2 - The bounding rectangle of the second element.
|
|
3164
|
+
* @returns {{
|
|
3165
|
+
* inDir: Dirs | null;
|
|
3166
|
+
* dirX: Dirs | null;
|
|
3167
|
+
* dirY: Dirs | null;
|
|
3168
|
+
* depthX: number;
|
|
3169
|
+
* depthY: number;
|
|
3170
|
+
* }} An object containing the collision direction and how deep the overlap is.
|
|
3171
|
+
*/
|
|
3172
|
+
function getElsCollDirDepth(rect1, rect2) {
|
|
3173
|
+
if (!areElsPerfColliding(rect1, rect2))
|
|
3174
|
+
return {
|
|
3175
|
+
inDir: null,
|
|
3176
|
+
dirX: null,
|
|
3177
|
+
dirY: null,
|
|
3178
|
+
depthX: 0,
|
|
3179
|
+
depthY: 0,
|
|
3180
|
+
};
|
|
3181
|
+
|
|
3182
|
+
const { overlapLeft, overlapRight, overlapTop, overlapBottom } = getElsCollOverlap(rect1, rect2);
|
|
3183
|
+
const { dirX, dirY } = getElsCollOverlapPos({
|
|
3184
|
+
overlapLeft,
|
|
3185
|
+
overlapRight,
|
|
3186
|
+
overlapTop,
|
|
3187
|
+
overlapBottom,
|
|
3188
|
+
});
|
|
3189
|
+
const depthX = Math.min(overlapLeft, overlapRight);
|
|
3190
|
+
const depthY = Math.min(overlapTop, overlapBottom);
|
|
3191
|
+
|
|
3192
|
+
/** @type {Dirs} */
|
|
3193
|
+
let inDir;
|
|
3194
|
+
|
|
3195
|
+
if (depthX < depthY) inDir = dirX;
|
|
3196
|
+
else inDir = dirY;
|
|
3197
|
+
return { inDir, dirX, dirY, depthX, depthY };
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
// Full detail report
|
|
3201
|
+
|
|
3202
|
+
/**
|
|
3203
|
+
* Detects the collision direction and depth between two DOMRects.
|
|
3204
|
+
*
|
|
3205
|
+
* @param {ObjRect} rect1 - The bounding rectangle of the first element.
|
|
3206
|
+
* @param {ObjRect} rect2 - The bounding rectangle of the second element.
|
|
3207
|
+
* @returns {{ depth: CollData; dirs: CollDirs; isNeg: NegCollDirs; }} Collision info or null if no collision is detected.
|
|
3208
|
+
*/
|
|
3209
|
+
function getElsCollDetails(rect1, rect2) {
|
|
3210
|
+
const isColliding = areElsPerfColliding(rect1, rect2);
|
|
3211
|
+
|
|
3212
|
+
/** @type {CollDirs} */
|
|
3213
|
+
const dirs = { in: null, x: null, y: null };
|
|
3214
|
+
|
|
3215
|
+
/** @type {NegCollDirs} */
|
|
3216
|
+
const isNeg = { y: null, x: null };
|
|
3217
|
+
|
|
3218
|
+
/** @type {Record<Dirs, number>} */
|
|
3219
|
+
const depth = { top: 0, bottom: 0, left: 0, right: 0 };
|
|
3220
|
+
|
|
3221
|
+
// Depth
|
|
3222
|
+
// Yes, it's actually reversed the values orders
|
|
3223
|
+
const { overlapLeft, overlapRight, overlapTop, overlapBottom } = getElsCollOverlap(rect2, rect1);
|
|
3224
|
+
depth.top = overlapTop;
|
|
3225
|
+
depth.bottom = overlapBottom;
|
|
3226
|
+
depth.left = overlapLeft;
|
|
3227
|
+
depth.right = overlapRight;
|
|
3228
|
+
|
|
3229
|
+
// Dirs
|
|
3230
|
+
/**
|
|
3231
|
+
* Detect the direction with the smallest positive overlap (entry point)
|
|
3232
|
+
* @type {[Dirs, number][]}
|
|
3233
|
+
*/
|
|
3234
|
+
// @ts-ignore
|
|
3235
|
+
const entries = Object.entries(depth)
|
|
3236
|
+
.filter(([, val]) => val > 0)
|
|
3237
|
+
.sort((a, b) => a[1] - b[1]);
|
|
3238
|
+
|
|
3239
|
+
// Yes, it's actually reversed the values orders here too
|
|
3240
|
+
const { dirX, dirY } = getElsCollOverlapPos({
|
|
3241
|
+
overlapLeft: overlapRight,
|
|
3242
|
+
overlapRight: overlapLeft,
|
|
3243
|
+
overlapTop: overlapBottom,
|
|
3244
|
+
overlapBottom: overlapTop,
|
|
3245
|
+
});
|
|
3246
|
+
dirs.y = dirY;
|
|
3247
|
+
dirs.x = dirX;
|
|
3248
|
+
|
|
3249
|
+
// isNeg
|
|
3250
|
+
if (depth.bottom < 0) isNeg.y = 'bottom';
|
|
3251
|
+
else if (depth.top < 0) isNeg.y = 'top';
|
|
3252
|
+
if (depth.left < 0) isNeg.x = 'left';
|
|
3253
|
+
else if (depth.right < 0) isNeg.x = 'right';
|
|
3254
|
+
|
|
3255
|
+
// Inside Dir
|
|
3256
|
+
dirs.in = isColliding
|
|
3257
|
+
? depth.top === depth.bottom && depth.bottom === depth.left && depth.left === depth.right
|
|
3258
|
+
? 'center'
|
|
3259
|
+
: entries.length
|
|
3260
|
+
? entries[0][0]
|
|
3261
|
+
: 'top'
|
|
3262
|
+
: null; // fallback in case of exact match
|
|
3263
|
+
|
|
3264
|
+
// Complete
|
|
3265
|
+
return { dirs, depth, isNeg };
|
|
3266
|
+
}
|
|
3267
|
+
|
|
2851
3268
|
;// ./src/v1/basics/html.mjs
|
|
2852
3269
|
|
|
2853
3270
|
|
|
3271
|
+
|
|
2854
3272
|
/**
|
|
2855
3273
|
* Checks if two DOM elements are colliding on the screen.
|
|
2856
3274
|
*
|
|
@@ -2861,44 +3279,157 @@ extendObjType([
|
|
|
2861
3279
|
function areHtmlElsColliding(elem1, elem2) {
|
|
2862
3280
|
const rect1 = elem1.getBoundingClientRect();
|
|
2863
3281
|
const rect2 = elem2.getBoundingClientRect();
|
|
3282
|
+
return collision_areElsColliding(rect1, rect2);
|
|
3283
|
+
}
|
|
2864
3284
|
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
3285
|
+
/**
|
|
3286
|
+
* Checks if two DOM elements are colliding on the screen, and locks the collision
|
|
3287
|
+
* until the element exits through the same side it entered.
|
|
3288
|
+
*
|
|
3289
|
+
* @param {Element} elem1 - First DOM element (e.g. draggable or moving element).
|
|
3290
|
+
* @param {Element} elem2 - Second DOM element (e.g. a container or boundary element).
|
|
3291
|
+
* @param {'top'|'bottom'|'left'|'right'} lockDirection - Direction that must be respected to unlock the collision.
|
|
3292
|
+
* @param {WeakMap<Element, string>} stateMap - A shared WeakMap to track persistent entry direction per element.
|
|
3293
|
+
* @returns {boolean} True if collision is still active.
|
|
3294
|
+
*/
|
|
3295
|
+
function areHtmlElsCollidingWithLock(elem1, elem2, lockDirection, stateMap) {
|
|
3296
|
+
const rect1 = elem1.getBoundingClientRect();
|
|
3297
|
+
const rect2 = elem2.getBoundingClientRect();
|
|
3298
|
+
const isColliding = areElsColliding(rect1, rect2);
|
|
3299
|
+
|
|
3300
|
+
if (isColliding) {
|
|
3301
|
+
// Save entry direction
|
|
3302
|
+
if (!stateMap.has(elem1)) {
|
|
3303
|
+
stateMap.set(elem1, lockDirection);
|
|
3304
|
+
}
|
|
3305
|
+
return true;
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
// Handle unlock logic
|
|
3309
|
+
if (stateMap.has(elem1)) {
|
|
3310
|
+
const lastDirection = stateMap.get(elem1);
|
|
3311
|
+
|
|
3312
|
+
switch (lastDirection) {
|
|
3313
|
+
case 'top':
|
|
3314
|
+
if (areElsCollTop(rect1, rect2)) stateMap.delete(elem1); // exited from top
|
|
3315
|
+
break;
|
|
3316
|
+
case 'bottom':
|
|
3317
|
+
if (areElsCollBottom(rect1, rect2)) stateMap.delete(elem1); // exited from bottom
|
|
3318
|
+
break;
|
|
3319
|
+
case 'left':
|
|
3320
|
+
if (areElsCollLeft(rect1, rect2)) stateMap.delete(elem1); // exited from left
|
|
3321
|
+
break;
|
|
3322
|
+
case 'right':
|
|
3323
|
+
if (areElsCollRight(rect1, rect2)) stateMap.delete(elem1); // exited from right
|
|
3324
|
+
break;
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
return stateMap.has(elem1); // still colliding (locked)
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
return false;
|
|
2871
3331
|
}
|
|
2872
3332
|
|
|
2873
3333
|
/**
|
|
2874
|
-
* Reads
|
|
2875
|
-
*
|
|
2876
|
-
* @param {File} file
|
|
2877
|
-
* @
|
|
3334
|
+
* Reads the contents of a file using the specified FileReader method.
|
|
3335
|
+
*
|
|
3336
|
+
* @param {File} file - The file to be read.
|
|
3337
|
+
* @param {'readAsArrayBuffer'|'readAsDataURL'|'readAsText'|'readAsBinaryString'} method -
|
|
3338
|
+
* The FileReader method to use for reading the file.
|
|
3339
|
+
* @returns {Promise<any>} - A promise that resolves with the file content, according to the chosen method.
|
|
3340
|
+
* @throws {Error} - If an unexpected error occurs while handling the result.
|
|
3341
|
+
* @throws {DOMException} - If the FileReader encounters an error while reading the file.
|
|
2878
3342
|
*/
|
|
2879
|
-
function
|
|
3343
|
+
function readFileBlob(file, method) {
|
|
2880
3344
|
return new Promise((resolve, reject) => {
|
|
2881
3345
|
const reader = new FileReader();
|
|
2882
|
-
|
|
2883
3346
|
reader.onload = () => {
|
|
2884
3347
|
try {
|
|
2885
|
-
|
|
2886
|
-
const result = JSON.parse(reader.result);
|
|
2887
|
-
resolve(result);
|
|
3348
|
+
resolve(reader.result);
|
|
2888
3349
|
} catch (error) {
|
|
2889
|
-
|
|
2890
|
-
reject(new Error(`Invalid JSON in file: ${file.name}\n${error.message}`));
|
|
3350
|
+
reject(error);
|
|
2891
3351
|
}
|
|
2892
3352
|
};
|
|
2893
|
-
|
|
2894
3353
|
reader.onerror = () => {
|
|
2895
|
-
reject(
|
|
3354
|
+
reject(reader.error);
|
|
2896
3355
|
};
|
|
3356
|
+
reader[method](file);
|
|
3357
|
+
});
|
|
3358
|
+
}
|
|
2897
3359
|
|
|
2898
|
-
|
|
3360
|
+
/**
|
|
3361
|
+
* Reads a file as a Base64 string using FileReader, and optionally formats it as a full data URL.
|
|
3362
|
+
*
|
|
3363
|
+
* Performs strict validation to ensure the result is a valid Base64 string or a proper data URL.
|
|
3364
|
+
*
|
|
3365
|
+
* @param {File} file - The file to be read.
|
|
3366
|
+
* @param {boolean|string} [isDataUrl=false] - If true, returns a full data URL; if false, returns only the Base64 string;
|
|
3367
|
+
* if a string is passed, it is used as the MIME type in the data URL.
|
|
3368
|
+
* @returns {Promise<string>} - A promise that resolves with the Base64 string or data URL.
|
|
3369
|
+
*
|
|
3370
|
+
* @throws {TypeError} - If the result is not a string or if `isDataUrl` is not a valid type.
|
|
3371
|
+
* @throws {Error} - If the result does not match the expected data URL format or Base64 structure.
|
|
3372
|
+
* @throws {DOMException} - If the FileReader fails to read the file.
|
|
3373
|
+
*/
|
|
3374
|
+
function readBase64Blob(file, isDataUrl = false) {
|
|
3375
|
+
return new Promise((resolve, reject) => {
|
|
3376
|
+
if (typeof isDataUrl !== 'string' && typeof isDataUrl !== 'boolean')
|
|
3377
|
+
reject(new TypeError('The isDataUrl parameter must be a boolean or a string.'));
|
|
3378
|
+
readFileBlob(file, 'readAsDataURL')
|
|
3379
|
+
.then(
|
|
3380
|
+
/**
|
|
3381
|
+
* Ensure that the URL format is correct in the required pattern
|
|
3382
|
+
* @param {string} base64Data
|
|
3383
|
+
*/ (base64Data) => {
|
|
3384
|
+
if (typeof base64Data !== 'string')
|
|
3385
|
+
throw new TypeError('Expected file content to be a string.');
|
|
3386
|
+
|
|
3387
|
+
const match = base64Data.match(/^data:(.+);base64,(.*)$/);
|
|
3388
|
+
if (!match || !match[2])
|
|
3389
|
+
throw new Error('Invalid data URL format or missing Base64 content.');
|
|
3390
|
+
const [, mimeType, base64] = match;
|
|
3391
|
+
if (!/^[\w/+]+=*$/.test(base64)) throw new Error('Base64 content is malformed.');
|
|
3392
|
+
|
|
3393
|
+
if (typeof isDataUrl === 'boolean') return resolve(isDataUrl ? base64Data : base64);
|
|
3394
|
+
if (!/^[\w-]+\/[\w.+-]+$/.test(isDataUrl))
|
|
3395
|
+
throw new Error(`Invalid MIME type string: ${isDataUrl}`);
|
|
3396
|
+
|
|
3397
|
+
return resolve(`data:${isDataUrl};base64,${base64}`);
|
|
3398
|
+
},
|
|
3399
|
+
)
|
|
3400
|
+
.catch(reject);
|
|
2899
3401
|
});
|
|
2900
3402
|
}
|
|
2901
3403
|
|
|
3404
|
+
/**
|
|
3405
|
+
* Reads a file and strictly validates its content as proper JSON using FileReader.
|
|
3406
|
+
*
|
|
3407
|
+
* Performs several checks to ensure the file contains valid, parsable JSON data.
|
|
3408
|
+
*
|
|
3409
|
+
* @param {File} file - The file to be read. It must contain valid JSON as plain text.
|
|
3410
|
+
* @returns {Promise<Record<string|number|symbol, any>|any[]>} - A promise that resolves with the parsed JSON object.
|
|
3411
|
+
*
|
|
3412
|
+
* @throws {SyntaxError} - If the file content is not valid JSON syntax.
|
|
3413
|
+
* @throws {TypeError} - If the result is not a string or does not represent a JSON value.
|
|
3414
|
+
* @throws {Error} - If the result is empty or structurally invalid as JSON.
|
|
3415
|
+
* @throws {DOMException} - If the FileReader fails to read the file.
|
|
3416
|
+
*/
|
|
3417
|
+
function readJsonBlob(file) {
|
|
3418
|
+
return new Promise((resolve, reject) =>
|
|
3419
|
+
readFileBlob(file, 'readAsText')
|
|
3420
|
+
.then((data) => {
|
|
3421
|
+
if (typeof data !== 'string') throw new TypeError('Expected file content to be a string.');
|
|
3422
|
+
const trimmed = data.trim();
|
|
3423
|
+
if (trimmed.length === 0) throw new Error('File is empty or contains only whitespace.');
|
|
3424
|
+
const parsed = JSON.parse(trimmed);
|
|
3425
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
3426
|
+
throw new Error('Parsed content is not a valid JSON object or array.');
|
|
3427
|
+
resolve(parsed);
|
|
3428
|
+
})
|
|
3429
|
+
.catch(reject),
|
|
3430
|
+
);
|
|
3431
|
+
}
|
|
3432
|
+
|
|
2902
3433
|
/**
|
|
2903
3434
|
* Saves a JSON object as a downloadable file.
|
|
2904
3435
|
* @param {string} filename
|
|
@@ -3001,7 +3532,8 @@ async function fetchJson(
|
|
|
3001
3532
|
|
|
3002
3533
|
const data = await response.json();
|
|
3003
3534
|
|
|
3004
|
-
if (!
|
|
3535
|
+
if (!Array.isArray(data) && !isJsonObject(data))
|
|
3536
|
+
throw new Error('Received invalid data instead of valid JSON.');
|
|
3005
3537
|
|
|
3006
3538
|
return data;
|
|
3007
3539
|
} catch (err) {
|
|
@@ -3103,19 +3635,34 @@ const getHtmlElPadding = (el) => {
|
|
|
3103
3635
|
|
|
3104
3636
|
/**
|
|
3105
3637
|
* Installs a script that toggles CSS classes on a given element
|
|
3106
|
-
* based on the page's visibility or focus state
|
|
3638
|
+
* based on the page's visibility or focus state, and optionally
|
|
3639
|
+
* triggers callbacks on visibility changes.
|
|
3107
3640
|
*
|
|
3108
3641
|
* @param {Object} [settings={}]
|
|
3109
3642
|
* @param {HTMLElement} [settings.element=document.body] - The element to receive visibility classes.
|
|
3110
3643
|
* @param {string} [settings.hiddenClass='windowHidden'] - CSS class applied when the page is hidden.
|
|
3111
3644
|
* @param {string} [settings.visibleClass='windowVisible'] - CSS class applied when the page is visible.
|
|
3645
|
+
* @param {() => void} [settings.onVisible] - Callback called when page becomes visible.
|
|
3646
|
+
* @param {() => void} [settings.onHidden] - Callback called when page becomes hidden.
|
|
3112
3647
|
* @returns {() => void} Function that removes all installed event listeners.
|
|
3648
|
+
* @throws {TypeError} If any provided setting is invalid.
|
|
3113
3649
|
*/
|
|
3114
3650
|
function installWindowHiddenScript({
|
|
3115
3651
|
element = document.body,
|
|
3116
3652
|
hiddenClass = 'windowHidden',
|
|
3117
3653
|
visibleClass = 'windowVisible',
|
|
3654
|
+
onVisible,
|
|
3655
|
+
onHidden,
|
|
3118
3656
|
} = {}) {
|
|
3657
|
+
if (!(element instanceof HTMLElement))
|
|
3658
|
+
throw new TypeError(`"element" must be an instance of HTMLElement.`);
|
|
3659
|
+
if (typeof hiddenClass !== 'string') throw new TypeError(`"hiddenClass" must be a string.`);
|
|
3660
|
+
if (typeof visibleClass !== 'string') throw new TypeError(`"visibleClass" must be a string.`);
|
|
3661
|
+
if (onVisible !== undefined && typeof onVisible !== 'function')
|
|
3662
|
+
throw new TypeError(`"onVisible" must be a function if provided.`);
|
|
3663
|
+
if (onHidden !== undefined && typeof onHidden !== 'function')
|
|
3664
|
+
throw new TypeError(`"onHidden" must be a function if provided.`);
|
|
3665
|
+
|
|
3119
3666
|
const removeClass = () => {
|
|
3120
3667
|
element.classList.remove(hiddenClass);
|
|
3121
3668
|
element.classList.remove(visibleClass);
|
|
@@ -3123,8 +3670,6 @@ function installWindowHiddenScript({
|
|
|
3123
3670
|
|
|
3124
3671
|
/** @type {string|null} */
|
|
3125
3672
|
let hiddenProp = null;
|
|
3126
|
-
/** @type {(this: any, evt: Event) => void} */
|
|
3127
|
-
let handler;
|
|
3128
3673
|
|
|
3129
3674
|
const visibilityEvents = [
|
|
3130
3675
|
'visibilitychange',
|
|
@@ -3142,7 +3687,8 @@ function installWindowHiddenScript({
|
|
|
3142
3687
|
}
|
|
3143
3688
|
}
|
|
3144
3689
|
|
|
3145
|
-
|
|
3690
|
+
/** @type {(this: any, evt: Event) => void} */
|
|
3691
|
+
const handler = function (evt) {
|
|
3146
3692
|
removeClass();
|
|
3147
3693
|
|
|
3148
3694
|
const type = evt?.type;
|
|
@@ -3154,10 +3700,18 @@ function installWindowHiddenScript({
|
|
|
3154
3700
|
|
|
3155
3701
|
if (visibleEvents.includes(type)) {
|
|
3156
3702
|
element.classList.add(visibleClass);
|
|
3703
|
+
onVisible?.();
|
|
3157
3704
|
} else if (hiddenEvents.includes(type)) {
|
|
3158
3705
|
element.classList.add(hiddenClass);
|
|
3706
|
+
onHidden?.();
|
|
3159
3707
|
} else {
|
|
3160
|
-
|
|
3708
|
+
if (isHidden) {
|
|
3709
|
+
element.classList.add(hiddenClass);
|
|
3710
|
+
onHidden?.();
|
|
3711
|
+
} else {
|
|
3712
|
+
element.classList.add(visibleClass);
|
|
3713
|
+
onVisible?.();
|
|
3714
|
+
}
|
|
3161
3715
|
}
|
|
3162
3716
|
};
|
|
3163
3717
|
|
|
@@ -3194,6 +3748,7 @@ function installWindowHiddenScript({
|
|
|
3194
3748
|
};
|
|
3195
3749
|
}
|
|
3196
3750
|
|
|
3751
|
+
// Trigger initial state
|
|
3197
3752
|
// @ts-ignore
|
|
3198
3753
|
const simulatedEvent = new Event(hiddenProp && document[hiddenProp] ? 'blur' : 'focus');
|
|
3199
3754
|
handler(simulatedEvent);
|
|
@@ -3201,6 +3756,38 @@ function installWindowHiddenScript({
|
|
|
3201
3756
|
return uninstall;
|
|
3202
3757
|
}
|
|
3203
3758
|
|
|
3759
|
+
/**
|
|
3760
|
+
* Checks if the given element is at least partially visible in the viewport.
|
|
3761
|
+
*
|
|
3762
|
+
* @param {HTMLElement} element - The DOM element to check.
|
|
3763
|
+
* @returns {boolean} True if the element is partially in the viewport, false otherwise.
|
|
3764
|
+
*/
|
|
3765
|
+
function isInViewport(element) {
|
|
3766
|
+
const elementTop = element.offsetTop;
|
|
3767
|
+
const elementBottom = elementTop + element.offsetHeight;
|
|
3768
|
+
|
|
3769
|
+
const viewportTop = window.scrollY;
|
|
3770
|
+
const viewportBottom = viewportTop + window.innerHeight;
|
|
3771
|
+
|
|
3772
|
+
return elementBottom > viewportTop && elementTop < viewportBottom;
|
|
3773
|
+
}
|
|
3774
|
+
|
|
3775
|
+
/**
|
|
3776
|
+
* Checks if the given element is fully visible in the viewport (top and bottom).
|
|
3777
|
+
*
|
|
3778
|
+
* @param {HTMLElement} element - The DOM element to check.
|
|
3779
|
+
* @returns {boolean} True if the element is fully visible in the viewport, false otherwise.
|
|
3780
|
+
*/
|
|
3781
|
+
function isScrolledIntoView(element) {
|
|
3782
|
+
const viewportTop = window.scrollY;
|
|
3783
|
+
const viewportBottom = viewportTop + window.innerHeight;
|
|
3784
|
+
|
|
3785
|
+
const elemTop = element.offsetTop;
|
|
3786
|
+
const elemBottom = elemTop + element.offsetHeight;
|
|
3787
|
+
|
|
3788
|
+
return elemBottom <= viewportBottom && elemTop >= viewportTop;
|
|
3789
|
+
}
|
|
3790
|
+
|
|
3204
3791
|
;// ./src/v1/basics/fullScreen.mjs
|
|
3205
3792
|
/**
|
|
3206
3793
|
* Checks if the document is currently in fullscreen mode.
|
|
@@ -3556,6 +4143,48 @@ function addAiMarkerShortcut(key = 'a') {
|
|
|
3556
4143
|
});
|
|
3557
4144
|
}
|
|
3558
4145
|
|
|
4146
|
+
/**
|
|
4147
|
+
* Trims a text string to a specified character limit, attempting to avoid cutting words in half.
|
|
4148
|
+
* If a space is found before the limit and it's not too far from the limit (at least 60%),
|
|
4149
|
+
* the cut is made at that space; otherwise, the text is hard-cut at the limit.
|
|
4150
|
+
* If the input is shorter than the limit, it is returned unchanged.
|
|
4151
|
+
*
|
|
4152
|
+
* @param {string} text - The input text to be trimmed.
|
|
4153
|
+
* @param {number} limit - The maximum number of characters allowed.
|
|
4154
|
+
* @param {number} [safeCutZone=0.6] - A decimal between 0 and 1 representing the minimal acceptable position
|
|
4155
|
+
* (as a fraction of `limit`) to cut at a space. Defaults to 0.6.
|
|
4156
|
+
* @returns {string} - The trimmed text, possibly ending with an ellipsis ("...").
|
|
4157
|
+
* @throws {TypeError} - Throws if `text` is not a string.
|
|
4158
|
+
* @throws {TypeError} - Throws if `limit` is not a positive integer.
|
|
4159
|
+
* @throws {TypeError} - Throws if `safeCutZone` is not a number between 0 and 1 (inclusive).
|
|
4160
|
+
*/
|
|
4161
|
+
function safeTextTrim(text, limit, safeCutZone = 0.6) {
|
|
4162
|
+
if (typeof text !== 'string')
|
|
4163
|
+
throw new TypeError(`Expected a string for 'text', but received ${typeof text}`);
|
|
4164
|
+
if (!Number.isInteger(limit) || limit <= 0)
|
|
4165
|
+
throw new TypeError(`Expected 'limit' to be a positive integer, but received ${limit}`);
|
|
4166
|
+
if (typeof safeCutZone !== 'number' || safeCutZone < 0 || safeCutZone > 1)
|
|
4167
|
+
throw new TypeError(
|
|
4168
|
+
`Expected 'safeCutZone' to be a number between 0 and 1, but received ${safeCutZone}`,
|
|
4169
|
+
);
|
|
4170
|
+
|
|
4171
|
+
let result = text.trim();
|
|
4172
|
+
if (result.length > limit) {
|
|
4173
|
+
// Try to cut the string into a space before the limit
|
|
4174
|
+
const safeCut = result.lastIndexOf(' ', limit);
|
|
4175
|
+
|
|
4176
|
+
if (safeCut > 0 && safeCut >= limit * safeCutZone) {
|
|
4177
|
+
// Only cuts where there is a space, and if the cut is not too early
|
|
4178
|
+
return `${result.substring(0, safeCut).trim()}...`;
|
|
4179
|
+
} else {
|
|
4180
|
+
// Emergency: Cuts straight to the limit and adds "...".
|
|
4181
|
+
return `${result.substring(0, limit).trim()}...`;
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4185
|
+
return result;
|
|
4186
|
+
}
|
|
4187
|
+
|
|
3559
4188
|
/*
|
|
3560
4189
|
import { useEffect } from "react";
|
|
3561
4190
|
|
|
@@ -3593,6 +4222,7 @@ export default KeyPressHandler;
|
|
|
3593
4222
|
|
|
3594
4223
|
|
|
3595
4224
|
|
|
4225
|
+
|
|
3596
4226
|
})();
|
|
3597
4227
|
|
|
3598
4228
|
window.TinyBasicsEs = __webpack_exports__;
|