tiny-essentials 1.13.2 → 1.14.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 +160 -20
- package/dist/v1/TinyBasicsEs.min.js +1 -1
- package/dist/v1/TinyDragger.js +115 -20
- package/dist/v1/TinyEssentials.js +400 -20
- 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 +116 -20
- package/dist/v1/basics/html.cjs +117 -20
- package/dist/v1/basics/html.d.mts +46 -7
- package/dist/v1/basics/html.mjs +112 -19
- package/dist/v1/basics/index.cjs +3 -0
- package/dist/v1/basics/index.d.mts +4 -1
- package/dist/v1/basics/index.mjs +3 -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 +5 -0
- package/dist/v1/index.d.mts +5 -1
- package/dist/v1/index.mjs +4 -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 +4 -3
- package/docs/v1/basics/array.md +16 -43
- package/docs/v1/basics/html.md +90 -27
- package/docs/v1/basics/text.md +44 -14
- package/docs/v1/libs/TinyNotifications.md +189 -0
- package/package.json +1 -1
|
@@ -2890,6 +2890,7 @@ __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),
|
|
@@ -2942,6 +2943,8 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
2942
2943
|
objType: () => (/* reexport */ objType),
|
|
2943
2944
|
offFullScreenChange: () => (/* reexport */ offFullScreenChange),
|
|
2944
2945
|
onFullScreenChange: () => (/* reexport */ onFullScreenChange),
|
|
2946
|
+
readBase64Blob: () => (/* reexport */ readBase64Blob),
|
|
2947
|
+
readFileBlob: () => (/* reexport */ readFileBlob),
|
|
2945
2948
|
readJsonBlob: () => (/* reexport */ readJsonBlob),
|
|
2946
2949
|
readJsonFile: () => (/* reexport */ readJsonFile),
|
|
2947
2950
|
renameFileAddPrefixSuffix: () => (/* reexport */ renameFileAddPrefixSuffix),
|
|
@@ -2953,6 +2956,7 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
2953
2956
|
requestFullScreen: () => (/* reexport */ requestFullScreen),
|
|
2954
2957
|
restoreLatestBackup: () => (/* reexport */ restoreLatestBackup),
|
|
2955
2958
|
ruleOfThree: () => (/* reexport */ ruleOfThree),
|
|
2959
|
+
safeTextTrim: () => (/* reexport */ safeTextTrim),
|
|
2956
2960
|
saveJsonFile: () => (/* reexport */ saveJsonFile),
|
|
2957
2961
|
shuffleArray: () => (/* reexport */ shuffleArray),
|
|
2958
2962
|
toTitleCase: () => (/* reexport */ toTitleCase),
|
|
@@ -4236,6 +4240,48 @@ function addAiMarkerShortcut(key = 'a') {
|
|
|
4236
4240
|
});
|
|
4237
4241
|
}
|
|
4238
4242
|
|
|
4243
|
+
/**
|
|
4244
|
+
* Trims a text string to a specified character limit, attempting to avoid cutting words in half.
|
|
4245
|
+
* If a space is found before the limit and it's not too far from the limit (at least 60%),
|
|
4246
|
+
* the cut is made at that space; otherwise, the text is hard-cut at the limit.
|
|
4247
|
+
* If the input is shorter than the limit, it is returned unchanged.
|
|
4248
|
+
*
|
|
4249
|
+
* @param {string} text - The input text to be trimmed.
|
|
4250
|
+
* @param {number} limit - The maximum number of characters allowed.
|
|
4251
|
+
* @param {number} [safeCutZone=0.6] - A decimal between 0 and 1 representing the minimal acceptable position
|
|
4252
|
+
* (as a fraction of `limit`) to cut at a space. Defaults to 0.6.
|
|
4253
|
+
* @returns {string} - The trimmed text, possibly ending with an ellipsis ("...").
|
|
4254
|
+
* @throws {TypeError} - Throws if `text` is not a string.
|
|
4255
|
+
* @throws {TypeError} - Throws if `limit` is not a positive integer.
|
|
4256
|
+
* @throws {TypeError} - Throws if `safeCutZone` is not a number between 0 and 1 (inclusive).
|
|
4257
|
+
*/
|
|
4258
|
+
function safeTextTrim(text, limit, safeCutZone = 0.6) {
|
|
4259
|
+
if (typeof text !== 'string')
|
|
4260
|
+
throw new TypeError(`Expected a string for 'text', but received ${typeof text}`);
|
|
4261
|
+
if (!Number.isInteger(limit) || limit <= 0)
|
|
4262
|
+
throw new TypeError(`Expected 'limit' to be a positive integer, but received ${limit}`);
|
|
4263
|
+
if (typeof safeCutZone !== 'number' || safeCutZone < 0 || safeCutZone > 1)
|
|
4264
|
+
throw new TypeError(
|
|
4265
|
+
`Expected 'safeCutZone' to be a number between 0 and 1, but received ${safeCutZone}`,
|
|
4266
|
+
);
|
|
4267
|
+
|
|
4268
|
+
let result = text.trim();
|
|
4269
|
+
if (result.length > limit) {
|
|
4270
|
+
// Try to cut the string into a space before the limit
|
|
4271
|
+
const safeCut = result.lastIndexOf(' ', limit);
|
|
4272
|
+
|
|
4273
|
+
if (safeCut > 0 && safeCut >= limit * safeCutZone) {
|
|
4274
|
+
// Only cuts where there is a space, and if the cut is not too early
|
|
4275
|
+
return `${result.substring(0, safeCut).trim()}...`;
|
|
4276
|
+
} else {
|
|
4277
|
+
// Emergency: Cuts straight to the limit and adds "...".
|
|
4278
|
+
return `${result.substring(0, limit).trim()}...`;
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
return result;
|
|
4283
|
+
}
|
|
4284
|
+
|
|
4239
4285
|
/*
|
|
4240
4286
|
import { useEffect } from "react";
|
|
4241
4287
|
|
|
@@ -6166,34 +6212,105 @@ function areHtmlElsColliding(elem1, elem2) {
|
|
|
6166
6212
|
}
|
|
6167
6213
|
|
|
6168
6214
|
/**
|
|
6169
|
-
* Reads
|
|
6170
|
-
*
|
|
6171
|
-
* @param {File} file
|
|
6172
|
-
* @
|
|
6215
|
+
* Reads the contents of a file using the specified FileReader method.
|
|
6216
|
+
*
|
|
6217
|
+
* @param {File} file - The file to be read.
|
|
6218
|
+
* @param {'readAsArrayBuffer'|'readAsDataURL'|'readAsText'|'readAsBinaryString'} method -
|
|
6219
|
+
* The FileReader method to use for reading the file.
|
|
6220
|
+
* @returns {Promise<any>} - A promise that resolves with the file content, according to the chosen method.
|
|
6221
|
+
* @throws {Error} - If an unexpected error occurs while handling the result.
|
|
6222
|
+
* @throws {DOMException} - If the FileReader encounters an error while reading the file.
|
|
6173
6223
|
*/
|
|
6174
|
-
function
|
|
6224
|
+
function readFileBlob(file, method) {
|
|
6175
6225
|
return new Promise((resolve, reject) => {
|
|
6176
6226
|
const reader = new FileReader();
|
|
6177
|
-
|
|
6178
6227
|
reader.onload = () => {
|
|
6179
6228
|
try {
|
|
6180
|
-
|
|
6181
|
-
const result = JSON.parse(reader.result);
|
|
6182
|
-
resolve(result);
|
|
6229
|
+
resolve(reader.result);
|
|
6183
6230
|
} catch (error) {
|
|
6184
|
-
|
|
6185
|
-
reject(new Error(`Invalid JSON in file: ${file.name}\n${error.message}`));
|
|
6231
|
+
reject(error);
|
|
6186
6232
|
}
|
|
6187
6233
|
};
|
|
6188
|
-
|
|
6189
6234
|
reader.onerror = () => {
|
|
6190
|
-
reject(
|
|
6235
|
+
reject(reader.error);
|
|
6191
6236
|
};
|
|
6237
|
+
reader[method](file);
|
|
6238
|
+
});
|
|
6239
|
+
}
|
|
6192
6240
|
|
|
6193
|
-
|
|
6241
|
+
/**
|
|
6242
|
+
* Reads a file as a Base64 string using FileReader, and optionally formats it as a full data URL.
|
|
6243
|
+
*
|
|
6244
|
+
* Performs strict validation to ensure the result is a valid Base64 string or a proper data URL.
|
|
6245
|
+
*
|
|
6246
|
+
* @param {File} file - The file to be read.
|
|
6247
|
+
* @param {boolean|string} [isDataUrl=false] - If true, returns a full data URL; if false, returns only the Base64 string;
|
|
6248
|
+
* if a string is passed, it is used as the MIME type in the data URL.
|
|
6249
|
+
* @returns {Promise<string>} - A promise that resolves with the Base64 string or data URL.
|
|
6250
|
+
*
|
|
6251
|
+
* @throws {TypeError} - If the result is not a string or if `isDataUrl` is not a valid type.
|
|
6252
|
+
* @throws {Error} - If the result does not match the expected data URL format or Base64 structure.
|
|
6253
|
+
* @throws {DOMException} - If the FileReader fails to read the file.
|
|
6254
|
+
*/
|
|
6255
|
+
function readBase64Blob(file, isDataUrl = false) {
|
|
6256
|
+
return new Promise((resolve, reject) => {
|
|
6257
|
+
if (typeof isDataUrl !== 'string' && typeof isDataUrl !== 'boolean')
|
|
6258
|
+
reject(new TypeError('The isDataUrl parameter must be a boolean or a string.'));
|
|
6259
|
+
readFileBlob(file, 'readAsDataURL')
|
|
6260
|
+
.then(
|
|
6261
|
+
/**
|
|
6262
|
+
* Ensure that the URL format is correct in the required pattern
|
|
6263
|
+
* @param {string} base64Data
|
|
6264
|
+
*/ (base64Data) => {
|
|
6265
|
+
if (typeof base64Data !== 'string')
|
|
6266
|
+
throw new TypeError('Expected file content to be a string.');
|
|
6267
|
+
|
|
6268
|
+
const match = base64Data.match(/^data:(.+);base64,(.*)$/);
|
|
6269
|
+
if (!match || !match[2])
|
|
6270
|
+
throw new Error('Invalid data URL format or missing Base64 content.');
|
|
6271
|
+
const [, mimeType, base64] = match;
|
|
6272
|
+
if (!/^[\w/+]+=*$/.test(base64)) throw new Error('Base64 content is malformed.');
|
|
6273
|
+
|
|
6274
|
+
if (typeof isDataUrl === 'boolean') return resolve(isDataUrl ? base64Data : base64);
|
|
6275
|
+
if (!/^[\w-]+\/[\w.+-]+$/.test(isDataUrl))
|
|
6276
|
+
throw new Error(`Invalid MIME type string: ${isDataUrl}`);
|
|
6277
|
+
|
|
6278
|
+
return resolve(`data:${isDataUrl};base64,${base64}`);
|
|
6279
|
+
},
|
|
6280
|
+
)
|
|
6281
|
+
.catch(reject);
|
|
6194
6282
|
});
|
|
6195
6283
|
}
|
|
6196
6284
|
|
|
6285
|
+
/**
|
|
6286
|
+
* Reads a file and strictly validates its content as proper JSON using FileReader.
|
|
6287
|
+
*
|
|
6288
|
+
* Performs several checks to ensure the file contains valid, parsable JSON data.
|
|
6289
|
+
*
|
|
6290
|
+
* @param {File} file - The file to be read. It must contain valid JSON as plain text.
|
|
6291
|
+
* @returns {Promise<Record<string|number|symbol, any>|any[]>} - A promise that resolves with the parsed JSON object.
|
|
6292
|
+
*
|
|
6293
|
+
* @throws {SyntaxError} - If the file content is not valid JSON syntax.
|
|
6294
|
+
* @throws {TypeError} - If the result is not a string or does not represent a JSON value.
|
|
6295
|
+
* @throws {Error} - If the result is empty or structurally invalid as JSON.
|
|
6296
|
+
* @throws {DOMException} - If the FileReader fails to read the file.
|
|
6297
|
+
*/
|
|
6298
|
+
function readJsonBlob(file) {
|
|
6299
|
+
return new Promise((resolve, reject) =>
|
|
6300
|
+
readFileBlob(file, 'readAsText')
|
|
6301
|
+
.then((data) => {
|
|
6302
|
+
if (typeof data !== 'string') throw new TypeError('Expected file content to be a string.');
|
|
6303
|
+
const trimmed = data.trim();
|
|
6304
|
+
if (trimmed.length === 0) throw new Error('File is empty or contains only whitespace.');
|
|
6305
|
+
const parsed = JSON.parse(trimmed);
|
|
6306
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
6307
|
+
throw new Error('Parsed content is not a valid JSON object or array.');
|
|
6308
|
+
resolve(parsed);
|
|
6309
|
+
})
|
|
6310
|
+
.catch(reject),
|
|
6311
|
+
);
|
|
6312
|
+
}
|
|
6313
|
+
|
|
6197
6314
|
/**
|
|
6198
6315
|
* Saves a JSON object as a downloadable file.
|
|
6199
6316
|
* @param {string} filename
|
|
@@ -6296,7 +6413,8 @@ async function fetchJson(
|
|
|
6296
6413
|
|
|
6297
6414
|
const data = await response.json();
|
|
6298
6415
|
|
|
6299
|
-
if (!
|
|
6416
|
+
if (!Array.isArray(data) && !isJsonObject(data))
|
|
6417
|
+
throw new Error('Received invalid data instead of valid JSON.');
|
|
6300
6418
|
|
|
6301
6419
|
return data;
|
|
6302
6420
|
} catch (err) {
|
|
@@ -6398,19 +6516,34 @@ const getHtmlElPadding = (el) => {
|
|
|
6398
6516
|
|
|
6399
6517
|
/**
|
|
6400
6518
|
* Installs a script that toggles CSS classes on a given element
|
|
6401
|
-
* based on the page's visibility or focus state
|
|
6519
|
+
* based on the page's visibility or focus state, and optionally
|
|
6520
|
+
* triggers callbacks on visibility changes.
|
|
6402
6521
|
*
|
|
6403
6522
|
* @param {Object} [settings={}]
|
|
6404
6523
|
* @param {HTMLElement} [settings.element=document.body] - The element to receive visibility classes.
|
|
6405
6524
|
* @param {string} [settings.hiddenClass='windowHidden'] - CSS class applied when the page is hidden.
|
|
6406
6525
|
* @param {string} [settings.visibleClass='windowVisible'] - CSS class applied when the page is visible.
|
|
6526
|
+
* @param {() => void} [settings.onVisible] - Callback called when page becomes visible.
|
|
6527
|
+
* @param {() => void} [settings.onHidden] - Callback called when page becomes hidden.
|
|
6407
6528
|
* @returns {() => void} Function that removes all installed event listeners.
|
|
6529
|
+
* @throws {TypeError} If any provided setting is invalid.
|
|
6408
6530
|
*/
|
|
6409
6531
|
function installWindowHiddenScript({
|
|
6410
6532
|
element = document.body,
|
|
6411
6533
|
hiddenClass = 'windowHidden',
|
|
6412
6534
|
visibleClass = 'windowVisible',
|
|
6535
|
+
onVisible,
|
|
6536
|
+
onHidden,
|
|
6413
6537
|
} = {}) {
|
|
6538
|
+
if (!(element instanceof HTMLElement))
|
|
6539
|
+
throw new TypeError(`"element" must be an instance of HTMLElement.`);
|
|
6540
|
+
if (typeof hiddenClass !== 'string') throw new TypeError(`"hiddenClass" must be a string.`);
|
|
6541
|
+
if (typeof visibleClass !== 'string') throw new TypeError(`"visibleClass" must be a string.`);
|
|
6542
|
+
if (onVisible !== undefined && typeof onVisible !== 'function')
|
|
6543
|
+
throw new TypeError(`"onVisible" must be a function if provided.`);
|
|
6544
|
+
if (onHidden !== undefined && typeof onHidden !== 'function')
|
|
6545
|
+
throw new TypeError(`"onHidden" must be a function if provided.`);
|
|
6546
|
+
|
|
6414
6547
|
const removeClass = () => {
|
|
6415
6548
|
element.classList.remove(hiddenClass);
|
|
6416
6549
|
element.classList.remove(visibleClass);
|
|
@@ -6418,8 +6551,6 @@ function installWindowHiddenScript({
|
|
|
6418
6551
|
|
|
6419
6552
|
/** @type {string|null} */
|
|
6420
6553
|
let hiddenProp = null;
|
|
6421
|
-
/** @type {(this: any, evt: Event) => void} */
|
|
6422
|
-
let handler;
|
|
6423
6554
|
|
|
6424
6555
|
const visibilityEvents = [
|
|
6425
6556
|
'visibilitychange',
|
|
@@ -6437,7 +6568,8 @@ function installWindowHiddenScript({
|
|
|
6437
6568
|
}
|
|
6438
6569
|
}
|
|
6439
6570
|
|
|
6440
|
-
|
|
6571
|
+
/** @type {(this: any, evt: Event) => void} */
|
|
6572
|
+
const handler = function (evt) {
|
|
6441
6573
|
removeClass();
|
|
6442
6574
|
|
|
6443
6575
|
const type = evt?.type;
|
|
@@ -6449,10 +6581,18 @@ function installWindowHiddenScript({
|
|
|
6449
6581
|
|
|
6450
6582
|
if (visibleEvents.includes(type)) {
|
|
6451
6583
|
element.classList.add(visibleClass);
|
|
6584
|
+
onVisible?.();
|
|
6452
6585
|
} else if (hiddenEvents.includes(type)) {
|
|
6453
6586
|
element.classList.add(hiddenClass);
|
|
6587
|
+
onHidden?.();
|
|
6454
6588
|
} else {
|
|
6455
|
-
|
|
6589
|
+
if (isHidden) {
|
|
6590
|
+
element.classList.add(hiddenClass);
|
|
6591
|
+
onHidden?.();
|
|
6592
|
+
} else {
|
|
6593
|
+
element.classList.add(visibleClass);
|
|
6594
|
+
onVisible?.();
|
|
6595
|
+
}
|
|
6456
6596
|
}
|
|
6457
6597
|
};
|
|
6458
6598
|
|
|
@@ -6489,6 +6629,7 @@ function installWindowHiddenScript({
|
|
|
6489
6629
|
};
|
|
6490
6630
|
}
|
|
6491
6631
|
|
|
6632
|
+
// Trigger initial state
|
|
6492
6633
|
// @ts-ignore
|
|
6493
6634
|
const simulatedEvent = new Event(hiddenProp && document[hiddenProp] ? 'blur' : 'focus');
|
|
6494
6635
|
handler(simulatedEvent);
|
|
@@ -8514,6 +8655,244 @@ class TinyDomReadyManager {
|
|
|
8514
8655
|
|
|
8515
8656
|
/* harmony default export */ const libs_TinyDomReadyManager = (TinyDomReadyManager);
|
|
8516
8657
|
|
|
8658
|
+
;// ./src/v1/libs/TinyNotifications.mjs
|
|
8659
|
+
|
|
8660
|
+
|
|
8661
|
+
/**
|
|
8662
|
+
* A utility class to manage browser notifications with sound and custom behavior.
|
|
8663
|
+
* Useful for triggering system notifications with optional sound, avatar icon, body truncation, and click actions.
|
|
8664
|
+
*
|
|
8665
|
+
* @class
|
|
8666
|
+
*/
|
|
8667
|
+
class TinyNotifications {
|
|
8668
|
+
/** @type {boolean} Whether notifications are currently allowed by the user. */
|
|
8669
|
+
#allowed = false;
|
|
8670
|
+
|
|
8671
|
+
/** @type {boolean} Indicates whether the user has already requested permission at least once. */
|
|
8672
|
+
#permissionRequested = false;
|
|
8673
|
+
|
|
8674
|
+
/** @type {HTMLAudioElement|null} Audio element to play when a notification is triggered. */
|
|
8675
|
+
#audio = null;
|
|
8676
|
+
|
|
8677
|
+
/** @type {number} Maximum number of characters in the notification body. */
|
|
8678
|
+
#bodyLimit = 100;
|
|
8679
|
+
|
|
8680
|
+
/** @type {string|null} Default avatar icon URL for notifications. */
|
|
8681
|
+
#defaultIcon = null;
|
|
8682
|
+
|
|
8683
|
+
/** @type {(this: Notification, evt: Event) => any} Default handler when a notification is clicked. */
|
|
8684
|
+
#defaultOnClick;
|
|
8685
|
+
|
|
8686
|
+
/**
|
|
8687
|
+
* Constructs a new instance of TinyNotifications.
|
|
8688
|
+
*
|
|
8689
|
+
* @param {Object} [settings={}] - Optional settings to initialize the notification manager.
|
|
8690
|
+
* @param {string|HTMLAudioElement|null} [settings.audio] - Path or URL to the audio file for notification sounds.
|
|
8691
|
+
* @param {string|null} [settings.defaultIcon] - Default icon URL to be used in notifications.
|
|
8692
|
+
* @param {number} [settings.bodyLimit=100] - Maximum number of characters allowed in the notification body.
|
|
8693
|
+
* @param {(this: Notification, evt: Event) => any} [settings.defaultOnClick] - Default function to execute when a notification is clicked.
|
|
8694
|
+
* @throws {TypeError} If any of the parameters are of an invalid type.
|
|
8695
|
+
*/
|
|
8696
|
+
constructor({
|
|
8697
|
+
audio = null,
|
|
8698
|
+
defaultIcon = null,
|
|
8699
|
+
bodyLimit = 100,
|
|
8700
|
+
defaultOnClick = function (event) {
|
|
8701
|
+
event.preventDefault();
|
|
8702
|
+
if (window.focus) window.focus();
|
|
8703
|
+
this.close();
|
|
8704
|
+
},
|
|
8705
|
+
} = {}) {
|
|
8706
|
+
if (!(audio instanceof HTMLAudioElement) && typeof audio !== 'string' && audio !== null)
|
|
8707
|
+
throw new TypeError('audio must be an instance of HTMLAudioElement or null.');
|
|
8708
|
+
if (defaultIcon !== null && typeof defaultIcon !== 'string')
|
|
8709
|
+
throw new TypeError('defaultIcon must be a string or null.');
|
|
8710
|
+
if (!Number.isFinite(bodyLimit) || bodyLimit < 0)
|
|
8711
|
+
throw new TypeError('bodyLimit must be a non-negative number.');
|
|
8712
|
+
if (typeof defaultOnClick !== 'function')
|
|
8713
|
+
throw new TypeError('defaultOnClick must be a function.');
|
|
8714
|
+
|
|
8715
|
+
this.#audio = typeof audio !== 'string' ? audio : new Audio(audio);
|
|
8716
|
+
this.#defaultIcon = defaultIcon;
|
|
8717
|
+
this.#bodyLimit = bodyLimit;
|
|
8718
|
+
this.#defaultOnClick = defaultOnClick;
|
|
8719
|
+
}
|
|
8720
|
+
|
|
8721
|
+
/**
|
|
8722
|
+
* Requests permission from the user to show notifications.
|
|
8723
|
+
* Updates the internal `#allowed` flag.
|
|
8724
|
+
*
|
|
8725
|
+
* @returns {Promise<boolean>} Resolves to `true` if permission is granted, otherwise `false`.
|
|
8726
|
+
*/
|
|
8727
|
+
requestPerm() {
|
|
8728
|
+
const tinyThis = this;
|
|
8729
|
+
return new Promise((resolve, reject) => {
|
|
8730
|
+
if (tinyThis.isCompatible()) {
|
|
8731
|
+
if (Notification.permission === 'default') {
|
|
8732
|
+
Notification.requestPermission()
|
|
8733
|
+
.then((permission) => {
|
|
8734
|
+
this.#permissionRequested = true;
|
|
8735
|
+
tinyThis.#allowed = permission === 'granted';
|
|
8736
|
+
resolve(tinyThis.#allowed);
|
|
8737
|
+
})
|
|
8738
|
+
.catch(reject);
|
|
8739
|
+
} else {
|
|
8740
|
+
this.#permissionRequested = true;
|
|
8741
|
+
tinyThis.#allowed = Notification.permission === 'granted';
|
|
8742
|
+
resolve(tinyThis.#allowed);
|
|
8743
|
+
}
|
|
8744
|
+
} else {
|
|
8745
|
+
this.#permissionRequested = true;
|
|
8746
|
+
tinyThis.#allowed = false;
|
|
8747
|
+
resolve(false);
|
|
8748
|
+
}
|
|
8749
|
+
});
|
|
8750
|
+
}
|
|
8751
|
+
|
|
8752
|
+
/**
|
|
8753
|
+
* Checks if the Notification API is supported by the current browser.
|
|
8754
|
+
*
|
|
8755
|
+
* @returns {boolean} Returns `true` if notifications are supported, otherwise `false`.
|
|
8756
|
+
*/
|
|
8757
|
+
isCompatible() {
|
|
8758
|
+
return 'Notification' in window;
|
|
8759
|
+
}
|
|
8760
|
+
|
|
8761
|
+
/**
|
|
8762
|
+
* Sends a browser notification with the provided title and configuration.
|
|
8763
|
+
* Truncates the body if necessary and plays a sound if configured.
|
|
8764
|
+
*
|
|
8765
|
+
* @param {string} title - The title of the notification.
|
|
8766
|
+
* @param {NotificationOptions & { vibrate?: number[] }} [config={}] - Optional configuration for the notification.
|
|
8767
|
+
* @returns {Notification|null} The created `Notification` instance, or `null` if permission is not granted.
|
|
8768
|
+
* @throws {TypeError} If the title is not a string or config is not a valid object.
|
|
8769
|
+
*/
|
|
8770
|
+
send(title, config = {}) {
|
|
8771
|
+
if (!this.#permissionRequested)
|
|
8772
|
+
throw new Error('You must call requestPerm() before sending a notification.');
|
|
8773
|
+
if (typeof title !== 'string') throw new TypeError('title must be a string.');
|
|
8774
|
+
if (typeof config !== 'object' || config === null)
|
|
8775
|
+
throw new TypeError('config must be a non-null object.');
|
|
8776
|
+
|
|
8777
|
+
if (!this.#allowed) return null;
|
|
8778
|
+
|
|
8779
|
+
const { icon = this.#defaultIcon || undefined, vibrate = [200, 100, 200] } = config;
|
|
8780
|
+
const options = { ...config };
|
|
8781
|
+
if (typeof icon === 'string') options.icon = icon;
|
|
8782
|
+
if (Array.isArray(vibrate)) options.vibrate = vibrate;
|
|
8783
|
+
|
|
8784
|
+
if (typeof options.body === 'string')
|
|
8785
|
+
options.body = safeTextTrim(options.body, this.#bodyLimit);
|
|
8786
|
+
|
|
8787
|
+
const notification = new Notification(title, options);
|
|
8788
|
+
notification.addEventListener('show', () => {
|
|
8789
|
+
if (!(this.#audio instanceof HTMLAudioElement)) return;
|
|
8790
|
+
this.#audio.currentTime = 0;
|
|
8791
|
+
this.#audio.play().catch((err) => console.error(err));
|
|
8792
|
+
});
|
|
8793
|
+
|
|
8794
|
+
if (typeof this.#defaultOnClick === 'function')
|
|
8795
|
+
notification.addEventListener('click', this.#defaultOnClick);
|
|
8796
|
+
|
|
8797
|
+
return notification;
|
|
8798
|
+
}
|
|
8799
|
+
|
|
8800
|
+
// === Getters and Setters ===
|
|
8801
|
+
|
|
8802
|
+
/**
|
|
8803
|
+
* Whether the requestPerm() method was already called.
|
|
8804
|
+
* @returns {boolean}
|
|
8805
|
+
*/
|
|
8806
|
+
wasPermissionRequested() {
|
|
8807
|
+
return this.#permissionRequested;
|
|
8808
|
+
}
|
|
8809
|
+
|
|
8810
|
+
/**
|
|
8811
|
+
* Returns the current permission status.
|
|
8812
|
+
* @returns {boolean} `true` if permission was granted, otherwise `false`.
|
|
8813
|
+
*/
|
|
8814
|
+
isAllowed() {
|
|
8815
|
+
return this.#allowed;
|
|
8816
|
+
}
|
|
8817
|
+
|
|
8818
|
+
/**
|
|
8819
|
+
* Gets the current notification audio.
|
|
8820
|
+
* @returns {HTMLAudioElement|null} The sound element, or `null` if not set.
|
|
8821
|
+
*/
|
|
8822
|
+
getAudio() {
|
|
8823
|
+
return this.#audio;
|
|
8824
|
+
}
|
|
8825
|
+
|
|
8826
|
+
/**
|
|
8827
|
+
* Sets the audio element used for notification sounds.
|
|
8828
|
+
* @param {HTMLAudioElement|string|null} value - A valid `HTMLAudioElement` or `null` to disable sound.
|
|
8829
|
+
* @throws {TypeError} If the value is not an `HTMLAudioElement` or `null`.
|
|
8830
|
+
*/
|
|
8831
|
+
setAudio(value) {
|
|
8832
|
+
if (!(value instanceof HTMLAudioElement) && typeof value !== 'string' && value !== null)
|
|
8833
|
+
throw new TypeError('sound must be an instance of HTMLAudioElement or null.');
|
|
8834
|
+
this.#audio = typeof value !== 'string' ? value : new Audio(value);
|
|
8835
|
+
}
|
|
8836
|
+
|
|
8837
|
+
/**
|
|
8838
|
+
* Gets the maximum length of the notification body text.
|
|
8839
|
+
* @returns {number} Number of characters allowed.
|
|
8840
|
+
*/
|
|
8841
|
+
getBodyLimit() {
|
|
8842
|
+
return this.#bodyLimit;
|
|
8843
|
+
}
|
|
8844
|
+
|
|
8845
|
+
/**
|
|
8846
|
+
* Sets the maximum number of characters allowed in the notification body.
|
|
8847
|
+
* @param {number} value - A non-negative integer.
|
|
8848
|
+
* @throws {TypeError} If the value is not a valid non-negative number.
|
|
8849
|
+
*/
|
|
8850
|
+
setBodyLimit(value) {
|
|
8851
|
+
if (!Number.isFinite(value) || value < 0)
|
|
8852
|
+
throw new TypeError('bodyLimit must be a non-negative number.');
|
|
8853
|
+
this.#bodyLimit = value;
|
|
8854
|
+
}
|
|
8855
|
+
|
|
8856
|
+
/**
|
|
8857
|
+
* Gets the default avatar icon URL.
|
|
8858
|
+
* @returns {string|null} The URL string or `null`.
|
|
8859
|
+
*/
|
|
8860
|
+
getDefaultAvatar() {
|
|
8861
|
+
return this.#defaultIcon;
|
|
8862
|
+
}
|
|
8863
|
+
|
|
8864
|
+
/**
|
|
8865
|
+
* Sets the default avatar icon URL.
|
|
8866
|
+
* @param {string|null} value - A string URL or `null` to disable default icon.
|
|
8867
|
+
* @throws {TypeError} If the value is not a string or `null`.
|
|
8868
|
+
*/
|
|
8869
|
+
setDefaultAvatar(value) {
|
|
8870
|
+
if (!(typeof value === 'string' || value === null))
|
|
8871
|
+
throw new TypeError('defaultIcon must be a string or null.');
|
|
8872
|
+
this.#defaultIcon = value;
|
|
8873
|
+
}
|
|
8874
|
+
|
|
8875
|
+
/**
|
|
8876
|
+
* Gets the default click event handler for notifications.
|
|
8877
|
+
* @returns {(this: Notification, evt: Event) => any} The current click handler function.
|
|
8878
|
+
*/
|
|
8879
|
+
getDefaultOnClick() {
|
|
8880
|
+
return this.#defaultOnClick;
|
|
8881
|
+
}
|
|
8882
|
+
|
|
8883
|
+
/**
|
|
8884
|
+
* Sets the default click event handler for notifications.
|
|
8885
|
+
* @param {(this: Notification, evt: Event) => any} value - A function to handle the notification click event.
|
|
8886
|
+
* @throws {TypeError} If the value is not a function.
|
|
8887
|
+
*/
|
|
8888
|
+
setDefaultOnClick(value) {
|
|
8889
|
+
if (typeof value !== 'function') throw new TypeError('defaultOnClick must be a function.');
|
|
8890
|
+
this.#defaultOnClick = value;
|
|
8891
|
+
}
|
|
8892
|
+
}
|
|
8893
|
+
|
|
8894
|
+
/* harmony default export */ const libs_TinyNotifications = (TinyNotifications);
|
|
8895
|
+
|
|
8517
8896
|
;// ./src/v1/index.mjs
|
|
8518
8897
|
|
|
8519
8898
|
|
|
@@ -8539,6 +8918,7 @@ class TinyDomReadyManager {
|
|
|
8539
8918
|
|
|
8540
8919
|
|
|
8541
8920
|
|
|
8921
|
+
|
|
8542
8922
|
|
|
8543
8923
|
|
|
8544
8924
|
})();
|