├── docs ├── .nojekyll ├── CNAME ├── favicon.ico ├── favicons │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ └── site.webmanifest ├── _coverpage.md ├── future.md ├── _sidebar.md ├── introduction.md ├── index.html ├── starting.md ├── deprecated.md ├── usage.md └── license-text.md ├── .gitignore ├── nui ├── assets │ ├── utils.js │ ├── test.js │ ├── styles.css │ ├── script.js │ └── useHistory.js ├── custom.css ├── main.html └── SimpleNotification │ ├── notification.css │ └── notification.js ├── fxmanifest.lua ├── LICENSE ├── .github ├── ISSUE_TEMPLATE │ └── bug-report.md └── workflows │ └── stable_release.yml ├── config.lua ├── client ├── history.lua └── main.lua ├── server └── update.lua └── README.md /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | docs.tasoagc.dev -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | temp -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TasoOneAsia/t-notify/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /docs/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TasoOneAsia/t-notify/HEAD/docs/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /docs/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TasoOneAsia/t-notify/HEAD/docs/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /docs/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TasoOneAsia/t-notify/HEAD/docs/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TasoOneAsia/t-notify/HEAD/docs/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TasoOneAsia/t-notify/HEAD/docs/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /nui/assets/utils.js: -------------------------------------------------------------------------------- 1 | export const isBrowserEnv = () => !window.invokeNative; 2 | 3 | export const mockNuiMessage = (data) => 4 | window.dispatchEvent( 5 | new MessageEvent("message", { 6 | data, 7 | }) 8 | ); 9 | -------------------------------------------------------------------------------- /docs/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /docs/_coverpage.md: -------------------------------------------------------------------------------- 1 | # T-Notify Docsv1.3.0 2 | 3 | > A FiveM implementation of the lightweight 4 | [SimpleNotification](https://github.com/Glagan/SimpleNotification) 5 | library created by [Glagan](https://github.com/Glagan/) 6 | 7 | By TasoAGC 8 | 9 | [Quick Start](starting.md) 10 | [Repository](https://github.com/TasoOneAsia/t-notify) 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/future.md: -------------------------------------------------------------------------------- 1 | 2 | ## Features for the Future 3 | 4 | **Future Features** 5 | 6 | This resource may develop to adopt the following features depending on interest: 7 | - Toggleable focus state 8 | - Notification history 9 | - Notification service-collection bucket 10 | - Custom data to be carried within a notification 11 | 12 | **Already Implemented** 13 | - FontAwesome notifications (v1.4.0) 14 | - Persistent notifications (v1.3.0) 15 | - Custom CSS Styling (v1.2.0) 16 | -------------------------------------------------------------------------------- /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | fx_version 'cerulean' 2 | 3 | game 'gta5' 4 | 5 | lua54 'yes' 6 | use_fxv2_oal 'yes' 7 | 8 | author 'Taso' 9 | description 'A FiveM integration of the SimpleNotification.js library' 10 | version '2.1.0' 11 | repository 'https://github.com/TasoOneAsia/t-notify' 12 | 13 | client_scripts { 14 | 'config.lua', 15 | 'client/main.lua', 16 | 'client/history.lua' 17 | } 18 | 19 | server_script 'server/update.lua' 20 | 21 | ui_page 'nui/main.html' 22 | 23 | files { 24 | 'nui/main.html', 25 | 'nui/SimpleNotification/notification.css', 26 | 'nui/SimpleNotification/notification.js', 27 | 'nui/assets/*.js', 28 | 'nui/assets/styles.css', 29 | 'nui/custom.css' 30 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | T-Notify - A FiveM Notification System 2 | 3 | Copyright (C) 2021 - TasoOneAsia 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: "[Bug Report] - " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Release Version** 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. 14 | 15 | **To Reproduce** 16 | Steps to reproduce the behavior: 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Expected behavior** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Screenshots** 26 | If applicable, add screenshots to help explain your problem. 27 | 28 | **Debug Prints** 29 | Debug Prints can be turned on by changing the value of `local debugMode` in `main.lua` from *false* to *true*. 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /nui/custom.css: -------------------------------------------------------------------------------- 1 | /* Place Custom Styling in this File! */ 2 | 3 | /* Styling for Notification Positioning (Don't touch, unless you know what you are doing) */ 4 | 5 | /* To change the positioning for these positions, please use this section rather than overwriting present properties found in Notification.css */ 6 | 7 | .gn-top-right, 8 | .gn-bottom-right { 9 | } 10 | 11 | .gn-bottom-right { 12 | } 13 | 14 | .gn-top-left, 15 | .gn-bottom-left { 16 | } 17 | 18 | .gn-bottom-left { 19 | } 20 | 21 | .gn-top-center, 22 | .gn-bottom-center { 23 | } 24 | 25 | .gn-top-center { 26 | } 27 | 28 | .gn-bottom-center { 29 | } 30 | 31 | 32 | /* All custom styles have to use the prefix "gn-", place them below */ 33 | /* NOTE: Once added, you always need to set the "custom" property to true otherwise this style will not work properly. */ 34 | 35 | .gn-example { 36 | background-color: pink; 37 | color: black; 38 | text-shadow: 0 1px 1px white; 39 | } -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | - [**Introduction**]() 2 | - [Features](?id=features) 3 | - [Screenshots](?id=screenshots) 4 | - [Support](?id=support-and-news) 5 | 6 | - [**Getting Started**](starting.md) 7 | - [Download & Installation](starting?id=installation-amp-download) 8 | - [Config File](/starting?id=intial-config) 9 | 10 | - [**Usage**](usage.md) 11 | - [Base Styling](usage?id=base-styling) 12 | - [Custom Styling](usage?id=custom-classes-guide) 13 | - [Main Functions](usage?id=function-types) 14 | - [Function Types](usage?id=triggering-notifications) 15 | - [Object Properties](usage?id=object-properties) 16 | - [Examples](usage?id=examples) 17 | - [Markdown Formatting Tags](usage?id=markdown-formatting-tags) 18 | - [Color Formatting](usage?id=color-formatting) 19 | - [History Usage](usage?id=history-usage) 20 | - [**Deprecated**](deprecated.md) 21 | - [**Future Features**](future.md) 22 | - [**License**](license-text.md) -------------------------------------------------------------------------------- /config.lua: -------------------------------------------------------------------------------- 1 | cfg = { 2 | position = 'top-right', --Changes the position of the notifications 3 | maxNotifications = 0, --Max notifications to show on screen (0 indicates no limit) 4 | sound = { --Change the alert sound 5 | name = '5_SEC_WARNING', 6 | reference = 'HUD_MINI_GAME_SOUNDSET' 7 | }, 8 | animations = { 9 | insertAnimation = 'insert-right', --Possible animation types: 'insert-left', 'insert-right', 'insert-top', 'insert-bottom', 'fadein', 'scalein' and 'rotatein' 10 | insertDuration = 1000, -- Duration of the insert animation 11 | removeAnimation = 'fadeout', -- Possible animation types: 'fadeout', 'scaleout', 'rotateout' 12 | removeDuration = 600, -- Duration of the remove animation 13 | }, 14 | useHistory = false, --Use the history system 15 | historyPosition = 'middle-right', --Changes the position of the history 16 | historyCommand = 'notihistory', --Command to open the history 17 | debugMode = false -- Toggle developer prints 18 | } -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | # An Introduction to T-Notify 2 | ## Features 3 | 4 | * Notification queue system 5 | 6 | * Notification positioning 7 | 8 | * *Markdown-like* formatting 9 | 10 | * Sound alerts when notifications are triggered 11 | 12 | * User defined custom styling 13 | 14 | * Highly configurable 15 | 16 | * Configurable animation settings 17 | 18 | ## Screenshots 19 | 20 | ![Info styling](https://tasoagc.dev/u/trvQOP.png) 21 | ![Error styling](https://tasoagc.dev/u/dVReJl.png) 22 | ![Warning styling](https://tasoagc.dev/u/9Oh1es.png) 23 | ![Success styling](https://tasoagc.dev/u/aAweMy.png) 24 | ![Message styling](https://i.tasoagc.dev/YdJo) 25 | 26 | # Support and News 27 | 28 | Support and news regarding the progress of this resource can be found in various places, click on the links below: 29 | 30 | * [FiveM Forums](https://forum.cfx.re/t/release-standalone-t-notify-a-simple-and-highly-customizable-notification-system/1618779) 31 | * [Support Discord](https://discord.gg/YWJY36EVsm) 32 | * [GitHub](https://github.com/tasooneasia/t-notify) -------------------------------------------------------------------------------- /.github/workflows/stable_release.yml: -------------------------------------------------------------------------------- 1 | name: Release Publisher 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | 7 | jobs: 8 | create_stable_release: 9 | name: "Create a Stable Release" 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout source code 13 | uses: actions/checkout@v2.3.4 14 | with: 15 | fetch-depth: 0 16 | ref: ${{ github.ref }} 17 | 18 | - name: Get Version Tag 19 | id: get_version_tag 20 | run: echo ::set-output name=VERSION_TAG::${GITHUB_REF/refs\/tags\//} 21 | 22 | - name: Create Zip Release 23 | run: | 24 | mkdir -p ./temp/t-notify 25 | cp -r ./{nui,config.lua,deprecated.lua,fxmanifest.lua,main.lua,LICENSE,README.md,update.lua,version} ./temp/t-notify 26 | cd ./temp && zip -r t-notify-${{ steps.get_version_tag.outputs.VERSION_TAG }}.zip ./t-notify 27 | 28 | - name: Create and Upload Release 29 | uses: marvinpinto/action-automatic-releases@v1.2.1 30 | with: 31 | repo_token: ${{ secrets.GITHUB_TOKEN }} 32 | prerelease: false 33 | files: ./temp/t-notify-${{ steps.get_version_tag.outputs.VERSION_TAG }}.zip 34 | -------------------------------------------------------------------------------- /client/history.lua: -------------------------------------------------------------------------------- 1 | local cbHistory = nil 2 | 3 | if cfg.useHistory then 4 | local activeHistory = false 5 | 6 | local function setHistoryActivity(active) 7 | activeHistory = active 8 | SendNUIMessage({ 9 | type = 'history', 10 | visible = active 11 | }) 12 | SetNuiFocus(active, active) 13 | end 14 | 15 | RegisterCommand(cfg.historyCommand, function() 16 | setHistoryActivity(not activeHistory) 17 | end) 18 | 19 | RegisterNUICallback('historyClose', function() 20 | setHistoryActivity(false) 21 | end) 22 | 23 | exports('SetHistoryVisibility', setHistoryActivity) 24 | exports('GetHistoryVisibility', function() 25 | return activeHistory 26 | end) 27 | end 28 | 29 | local function RemoveNotification(id) 30 | if type(id) == 'number' then 31 | SendNUIMessage({ 32 | type = 'removeHistoryNoti', 33 | id = id 34 | }) 35 | end 36 | end 37 | 38 | exports('RemoveNotification', RemoveNotification) 39 | 40 | local function ClearHistory() 41 | SendNUIMessage({ 42 | type = 'clearHistory' 43 | }) 44 | end 45 | 46 | exports('ClearHistory', ClearHistory) 47 | 48 | local function GetHistory() 49 | cbHistory = promise.new() 50 | SendNUIMessage({ 51 | type = 'getHistory' 52 | }) 53 | 54 | return Citizen.Await(cbHistory) 55 | end 56 | 57 | exports('GetHistory', GetHistory) 58 | 59 | RegisterNUICallback('getHistory', function(data, cb) 60 | cbHistory:resolve(data) 61 | cbHistory = nil 62 | cb({}) 63 | end) -------------------------------------------------------------------------------- /server/update.lua: -------------------------------------------------------------------------------- 1 | CreateThread(function() 2 | local localName = GetCurrentResourceName() 3 | local resourceName = (localName == 't-notify') and "^2[t-notify]^0" or ("^2[t-notify] ^2(%s)^0"):format(localName) 4 | 5 | local function checkVersionHandler(respCode, respText) 6 | if respCode ~= 200 then 7 | print(("%s - Error in checking for update, error code %s"):format(resourceName, respCode)) 8 | return 9 | end 10 | 11 | local currVersion = GetResourceMetadata(localName, "version", 0) 12 | local latestVersion = json.decode(respText).tag_name:gsub("v", "") 13 | if currVersion > latestVersion then 14 | print(("You may be using a pre-release of %s. Your version: ^1%s^0, GitHub version: ^2%s^0."):format(resourceName, currVersion, latestVersion)) 15 | elseif currVersion < latestVersion then 16 | print("\n^1###############################\n") 17 | 18 | local updateText = [[ 19 | Your %s is currently ^1outdated^0. 20 | 21 | 22 | The latest stable version is ^2%s^0, your version is (^8%s^0) 23 | 24 | 25 | You can download the latest stable release from ^3https://github.com/TasoOneAsia/t-notify/releases/tag/%s^0 26 | ]] 27 | print(updateText:format(resourceName, latestVersion, currVersion, latestVersion)) 28 | 29 | print("^1############################### ^0") 30 | else 31 | local startTxtTmpl = "%s (v%s) is up to date and has started" 32 | print(startTxtTmpl:format(resourceName, latestVersion)) 33 | end 34 | end 35 | 36 | PerformHttpRequest("https://api.github.com/repos/TasoOneAsia/t-notify/releases/latest", checkVersionHandler, "GET") 37 | end) -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | T-Notify Docs 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
Please wait loading...
17 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /nui/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 17 |
18 |

Notification History

19 |
20 |
21 | 26 |

1 / 1

27 | 32 |
33 |
34 |
35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Discord](https://img.shields.io/discord/791854454760013827.svg?label=Support&logo=discord)](https://discord.gg/ewvbgb5) 2 | [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://opensource.org/licenses/gpl-3.0.html) 3 | [![Release](https://img.shields.io/github/v/release/tasooneasia/t-notify)](https://github.com/TasoOneAsia/t-notify/releases/) 4 | # T-Notify 5 | 6 | A FiveM implementation of the lightweight [SimpleNotification](https://github.com/Glagan/SimpleNotification) library created by [Glagan](https://github.com/Glagan/) 7 | 8 | Interested in updates to this release, require support, or are just curious about my other projects, 9 | join the Project-Error [Discord](https://discord.gg/YWJY36EVsm)! 10 | 11 | [Documentation](https://docs.tasoagc.dev) 12 | 13 | ## Features 14 | 15 | * Notification queue system 16 | 17 | * Notification positioning 18 | 19 | * *Markdown-like* formatting 20 | 21 | * Sound alerts when notifications are triggered 22 | 23 | * User defined custom styling 24 | 25 | * Persistent Notifications 26 | 27 | * Configurable animation settings 28 | 29 | ## Screenshots 30 | 31 | ![Info styling](https://forum.cfx.re/uploads/default/original/4X/3/d/6/3d64cd72444547661c99f8ee7ae5719260cda042.jpeg) 32 | ![Error styling](https://forum.cfx.re/uploads/default/original/4X/c/7/e/c7e58d639a69772a80ed1e74843d242c143e5df1.png) 33 | ![Warning styling](https://forum.cfx.re/uploads/default/original/4X/9/d/1/9d1d727fe490da11e9b603c4edb6f13cb109a09a.png) 34 | ![Successstyling](https://forum.cfx.re/uploads/default/original/4X/8/f/d/8fd0389a9edb1fa6733cbcc5c91bca7c914c80d5.png) 35 | 36 | 37 | ## Installation & Download 38 | 39 | **From Releases** 40 | * Visit [releases](https://github.com/TasoOneAsia/t-notify/releases/) 41 | * Download and unzip the latest release 42 | * Rename the directory to ``t-notify``, if not already. 43 | * Place ``t-notify`` in your ``resources`` directory 44 | 45 | **Using Git** 46 | 47 | cd resources 48 | git clone https://github.com/TasoOneAsia/t-notify.git t-notify 49 | 50 | 51 | **Start** 52 | 53 | Add the following to your server.cfg before any resources that have `t-notify` as a dependency 54 | 55 | ensure t-notify 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /docs/starting.md: -------------------------------------------------------------------------------- 1 | ## Installation & Download 2 | 3 | **From Releases** 4 | * Visit [releases](https://github.com/TasoOneAsia/t-notify/releases/) 5 | * Download and unzip the latest release 6 | * Rename the directory to ``t-notify``, if not already. 7 | * Place ``t-notify`` in your ``resources`` directory 8 | 9 | **Using Git** 10 | cd resources 11 | git clone https://github.com/TasoOneAsia/t-notify.git t-notify 12 | 13 | **Start** 14 | 15 | Add the following to your server.cfg before any resources that have `t-notify` as a dependency 16 | 17 | ensure t-notify 18 | 19 | ## Intial Config 20 | 21 | T-Notify includes a small config that allows for various changes to how the resource operates. This can be found in the ``config.lua`` file. 22 | 23 | cfg = { 24 | position = 'top-right', -- Changes the position of the notifications 25 | maxNotifications = 0, --Max notifications to show on screen (0 indicates no limit) 26 | sound = { -- Change the alert sound 27 | name = '5_SEC_WARNING', 28 | reference = 'HUD_MINI_GAME_SOUNDSET' 29 | }, 30 | animations = { 31 | insertAnimation = 'insert-right', -- Possible animation types: 'insert-left', 'insert-right', 'insert-top', 'insert-bottom', 'fadein', 'scalein' ,'rotatein' 32 | insertDuration = 1000, 33 | removeAnimation = 'fadeout', -- Possible animation types: 'fadeout', 'scaleout', 'rotateout' 34 | removeDuration = 600 35 | }, 36 | debugMode = true --Toggle developer prints 37 | } 38 | 39 | * **Position** - Will change the positioning of the notifications (top-left, top-center, top-right, bottom-left, bottom-center, bottom-right, middle-left, middle-right) 40 | * **maxNotifications** - The max number of notifications to show on-screen at once. 41 | * **Sound** - Allows for the change of the notification alert sound. Reference [this](https://wiki.gtanet.work/index.php?title=FrontEndSoundlist) for options. 42 | * *name* - Sound Name 43 | * *reference* - Sound Set Name 44 | * **Animations** - Allows for the customization of notification animations 45 | * *insertAnimation*- Insert animation ('insert-left', 'insert-right', 'insert-top', 'insert-bottom', 'fadein', 'scalein' and 'rotatein') 46 | * *insertDuration* - Insert animation duration in *ms* 47 | * *removeAnimation* - Remove animation ('fadeout', 'scaleout', 'rotateout') 48 | * *removeDuration* - Remove animation duration in *ms* 49 | * **debugMode** - Toggle showing developer prints in console. 50 | -------------------------------------------------------------------------------- /nui/assets/test.js: -------------------------------------------------------------------------------- 1 | import { playNotification } from "./script.js"; 2 | import { mockNuiMessage } from "./utils.js"; 3 | 4 | /** 5 | * Window debug method for browser testing 6 | * @param notiObject {NotiObject} 7 | **/ 8 | const testNotify = (notiObject) => { 9 | playNotification({ type: "noti", ...notiObject }); 10 | }; 11 | 12 | // Setup the environment for debugging 13 | export const registerWindowDebug = () => { 14 | /** @type {InitData} */ 15 | const browserConfig = { 16 | position: "top-right", 17 | insertAnim: "insert-right", 18 | insertDuration: 1000, 19 | removeAnim: "fadeout", 20 | removeDuration: 600, 21 | maxNotifications: 0, 22 | useHistory: true, 23 | historyPosition: "middle-right", 24 | }; 25 | 26 | mockNuiMessage({ 27 | type: "init", 28 | ...browserConfig, 29 | }); 30 | 31 | window.testNotify = testNotify; 32 | 33 | console.log( 34 | "%cT-Notify Browser Debug", 35 | "color: red; font-size: 30px; -webkit-text-stroke: 1px black; font-weight: bold;" 36 | ); 37 | 38 | const helpText = `%cWelcome to T-Notify's browser debugging tool. When running t-notify in browser, certain developer tools are automatically enabled.\n\n\`window.testNotify\` has been registered as a function. It accepts a object of type NotiObject.\n\ninterface NotiObject ${JSON.stringify({type: 'string', style: 'info | error | success', message: 'string', title: 'string', image: 'string', custom: 'boolean', position: 'top-right | top-left | bottom-left | bottom-right', duration: 'number'}, null, '\t')}`; 39 | console.log(helpText, "color: green; font-size: 15px"); 40 | 41 | const browserConfText = "%cWhen in browser, this is the default config:"; 42 | 43 | console.log( 44 | browserConfText, 45 | "color: green; font-size: 15px; font-weight: bold;" 46 | ); 47 | console.log( 48 | "%c" + JSON.stringify(browserConfig, null, "\t"), 49 | "font-size: 15px; color: green;" 50 | ); 51 | 52 | const exampleText = `%cHeres a simple example:\n\nwindow.testNotify({ style: 'info', message: 'test'})`; 53 | console.log(exampleText, "color: green; font-size: 15px; font-weight: bold;"); 54 | 55 | playNotification({ 56 | position: "top-right", 57 | type: "noti", 58 | style: "info", 59 | duration: 15000, 60 | title: 'Browser Debug', 61 | message: 62 | "Welcome to T-Notify in the browser, please open DevTools console for further info!", 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /docs/deprecated.md: -------------------------------------------------------------------------------- 1 | 2 | ### Server and Client Triggers (deprecated) 3 | 4 | >Under T-Notify v1.3.0, notifications were triggered using these methods. The current method uses an **Object** rather than regular parameters. 5 | 6 | ### Exports (Client-Side) 7 | 8 | * SendTextAlert (style, message, duration, sound, custom) 9 | * Style {STRING} (Required) - One of the available styles as listed in the **Styling** Section. 10 | * Message {STRING} (Required) - Message to display in the notification. 11 | * Duration {INTEGER} (Optional) - Duration to display notification in ms. *Defaults to 2500ms*. 12 | * Sound {BOOL} (Optional) - If true, the notification will also have an alert sound. *Defaults to false*. 13 | * Custom {BOOL} (Optional) - This ***must*** be set to true in order to utilize a custom style that wasn't present by default. *Defaults to false*. 14 | 15 | * SendAny (style, title, message, image, duration, sound, custom) 16 | * Style {STRING} (Required) - One of the available styles as listed above . 17 | * Title {STRING} (Optional) - Title to display in the notification. *Defaults to nil* 18 | * Message {STRING} (Optional) - Message to display in the notification. *Defaults to nil* 19 | * Image {STRING} (Optional) - Accepts an Image URL to embed into the notification. *Defaults to nil* 20 | * Duration {INTEGER} (Optional) - Duration to display notification in ms. *Defaults to 2500ms*. 21 | * Sound {BOOL} (Optional) - If true, the notification will also have an alert sound. *Defaults to false*. 22 | * Custom {BOOL} (Optional) - This ***must*** be set to true in order to utilize a custom style that wasn't present by default. *Defaults to false*. 23 | 24 | * SendImage (style, title, image, duration, sound, custom) 25 | * Style {STRING} (Required) - One of the available styles as listed above. 26 | * Title {STRING} (Optional) - Title to display in the notification. *Defaults to nil* 27 | * Image {STRING} (Required) - Accepts an Image URL to embed into the notification 28 | * Duration {INTEGER} (Optional) - Duration to display notification in ms. *Defaults to 2500ms*. 29 | * Sound {BOOL} (Optional) - If true, the notification will also have an alert sound. *Defaults to false*. 30 | * Custom {BOOL} (Optional) - This ***must*** be set to true in order to utilize a custom style that wasn't present by default. *Defaults to false*. 31 | 32 | #### Export Example (Lua) 33 | 34 | Here is an example of how to trigger a notification event using an `export` on the ***client-side*** 35 | 36 | ```lua 37 | -- This sends a notification with the 'info' styling, an example messsage, a duration of 5500ms, and an audio alert 38 | 39 | exports['t-notify']:SendTextAlert('info', 'This is an example message', 5500, true) 40 | ``` 41 | 42 | #### Trigger Client Events (Server-Side) 43 | 44 | * SendTextAlert ( style, message, duration, sound, custom) 45 | * Style {STRING} (Required) - One of the available styles as listed in the **Styling** Section. 46 | * Message {STRING} (Required) - Message to display in the notification. 47 | * Duration {NUMBER} (Optional) - Duration to display notification in ms. *Defaults to 2500ms*. 48 | * Sound {BOOL} (Optional) - If true, the notification will also have an alert sound. *Defaults to false*. 49 | * Custom {BOOL} (Optional) - This ***must*** be set to true in order to utilize a custom style that wasn't present by default. *Defaults to false.* 50 | 51 | * SendAny (style, title, message, image, duration, sound, custom) 52 | * Style {STRING} (Required) - One of the available styles as listed above . 53 | * Title {STRING} (Optional) - Title to display in the notification. *Defaults to nil* 54 | * Message {STRING} (Optional) - Message to display in the notification. *Defaults to nil* 55 | * Image {STRING} (Optional) - Accepts an Image URL to embed into the notification. *Defaults to nil* 56 | * Duration {NUMBER} (Optional) - Duration to display notification in ms. *Defaults to 2500ms*. 57 | * Sound {BOOL} (Optional) - If true, the notification will also have an alert sound. *Defaults to false*. 58 | * Custom {BOOL} (Optional) - This ***must*** be set to true in order to utilize a custom style that wasn't present by default. *Defaults to false*. 59 | 60 | * SendImage (style, title, image, duration, sound, custom) 61 | * Style {STRING} (Required) - One of the available styles as listed above . 62 | * Title {STRING} (Optional) - Title to display in the notification. *Defaults to nil* 63 | * Image {STRING} (Required) - Accepts an Image URL to embed into the notification 64 | * Duration {NUMBER} (Optional) - Duration to display notification in ms. *Defaults to 2500ms*. 65 | * Sound {BOOL} (Optional) - If true, the notification will also have an alert sound. *Defaults to false*. 66 | * Custom {BOOL} (Optional) - This ***must*** be set to true in order to utilize a custom style that wasn't present by default. *Defaults to false*. 67 | 68 | #### TriggerClientEvent Example (Server-Side) 69 | 70 | Here is an example on how to trigger a notification using a `TriggerClientEvent` on the ***server-side*** 71 | 72 | ``` lua 73 | local player = 'ServerID of receiving client' 74 | 75 | TriggerClientEvent('tnotify:client:SendTextAlert', player, { 76 | style = 'error', 77 | duration = 10500, 78 | message = 'Alert Test', 79 | sound = true 80 | }) 81 | ``` -------------------------------------------------------------------------------- /nui/assets/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | html, 7 | body { 8 | min-height: 100vh; 9 | overflow-x: hidden; 10 | overflow-y: auto; 11 | background: none; 12 | } 13 | 14 | body { 15 | color: white; 16 | text-shadow: 0 1px 1px black; 17 | } 18 | 19 | body .title { 20 | color: white; 21 | } 22 | 23 | a:hover { 24 | color: white; 25 | } 26 | 27 | h1 { 28 | font-size: 1.2em; 29 | } 30 | 31 | .corner-4, 32 | .custom { 33 | border-radius: 4px; 34 | border: 2px solid #4F6372; 35 | background-color: #3E4E5B; 36 | } 37 | 38 | .corner-4 { 39 | display: flex; 40 | flex-wrap: wrap; 41 | justify-content: space-between; 42 | margin-bottom: 0.75rem; 43 | } 44 | 45 | .corner-4>div { 46 | max-width: 50%; 47 | flex: 0 0 50%; 48 | } 49 | 50 | .columns { 51 | margin-top: 0; 52 | } 53 | 54 | .label { 55 | color: white; 56 | } 57 | 58 | .message { 59 | text-shadow: none; 60 | } 61 | 62 | .gn-cs-under { 63 | text-decoration: underline; 64 | } 65 | 66 | .history-wrapper { 67 | position: absolute; 68 | width: 100%; 69 | height: 100%; 70 | display: flex; 71 | overflow: hidden; 72 | } 73 | 74 | .history-search { 75 | position: relative; 76 | width: 22rem; 77 | display: none; 78 | margin-right: 1rem; 79 | margin-bottom: 1rem; 80 | } 81 | 82 | .history-search input { 83 | flex-grow: 1; 84 | border: 0; 85 | border-radius: 4px; 86 | color: white; 87 | font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif; 88 | font-size: 1.0rem; 89 | background-color: #0077bb; 90 | text-shadow: 0 1px 1px black; 91 | padding: 0.5rem 0.8rem; 92 | outline: none; 93 | margin-right: 0.5rem; 94 | box-shadow: 1px 1px 3px black; 95 | overflow: hidden; 96 | } 97 | 98 | .history-search input::placeholder { 99 | color: #d3d3d3; 100 | text-shadow: 0 1px 1px black; 101 | } 102 | 103 | .history-search button { 104 | height: 2.5rem; 105 | width: 2.5rem; 106 | border: 0; 107 | border-radius: 4px; 108 | background-color: #0077bb; 109 | color: white; 110 | font-size: 1.0rem; 111 | text-shadow: 0 1px 1px black; 112 | cursor: pointer; 113 | outline: none; 114 | box-shadow: 1px 1px 3px black; 115 | transition: background-color 0.1s ease-in-out; 116 | } 117 | 118 | .history-search button:hover { 119 | background-color: #0099ff; 120 | } 121 | 122 | .history-container { 123 | position: relative; 124 | display: none; 125 | flex-direction: column; 126 | width: 22rem; 127 | font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif; 128 | background-color: #0077bb; 129 | border-radius: 3px; 130 | box-shadow: 1px 1px 3px black; 131 | overflow: hidden; 132 | margin-right: 1rem; 133 | } 134 | 135 | .history-container>h1 { 136 | position: relative; 137 | display: flex; 138 | align-items: center; 139 | margin: 0; 140 | width: 100%; 141 | padding: 0.7rem; 142 | font-weight: bold; 143 | text-overflow: ellipsis; 144 | white-space: nowrap; 145 | background-color: rgba(0, 0, 0, 0.2); 146 | border-bottom: 1px solid rgba(0, 0, 0, 0.4); 147 | overflow: hidden; 148 | } 149 | 150 | .history-saved { 151 | position: relative; 152 | display: flex; 153 | flex-direction: column; 154 | justify-content: start; 155 | align-items: center; 156 | width: 100%; 157 | height: 100%; 158 | row-gap: 0.6rem; 159 | margin: 0.6rem 0; 160 | } 161 | 162 | .history-empty { 163 | position: relative; 164 | display: flex; 165 | align-items: center; 166 | justify-content: center; 167 | height: 6rem; 168 | font-weight: bold; 169 | font-size: 1.5rem; 170 | } 171 | 172 | .history-notification { 173 | position: relative; 174 | display: flex; 175 | flex-direction: column; 176 | align-items: start; 177 | justify-content: start; 178 | width: 90%; 179 | border-radius: 3px; 180 | box-shadow: 1px 1px 3px black; 181 | overflow: hidden; 182 | } 183 | 184 | .history-notification>h2 { 185 | margin: 0; 186 | font-size: 1rem; 187 | font-weight: bold; 188 | color: white; 189 | width: 100%; 190 | background-color: rgba(0, 0, 0, 0.2); 191 | padding: 0.5rem; 192 | border-bottom: 1px solid rgba(0, 0, 0, 0.4); 193 | } 194 | 195 | .history-notification div:last-child { 196 | display: flex; 197 | align-items: end; 198 | justify-content: space-between; 199 | width: 95%; 200 | padding: 0.5rem; 201 | } 202 | 203 | .history-notification p { 204 | margin: 0; 205 | font-size: 0.9rem; 206 | color: white; 207 | padding: 0.5rem 0.5rem; 208 | max-height: 1.5rem; 209 | } 210 | 211 | .history-notification span { 212 | font-size: 0.8rem; 213 | width: 100%; 214 | margin: 0; 215 | } 216 | 217 | .history-notification button { 218 | font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif; 219 | text-shadow: black 0 1px 1px; 220 | background-color: #e8403d; 221 | border: none; 222 | color: white; 223 | font-size: 0.8rem; 224 | font-weight: bold; 225 | border-radius: 2px; 226 | padding: 0.2rem 0.5rem; 227 | cursor: pointer; 228 | box-shadow: 1px 1px 3px black; 229 | transition: 0.1s 230 | } 231 | 232 | .history-notification button:hover { 233 | background-color: #f63a37; 234 | } 235 | 236 | .history-img-btn { 237 | background: #5d5d5d !important; 238 | font-size: 0.75rem !important; 239 | padding: 0.1rem 0.3rem !important; 240 | } 241 | 242 | .history-img-btn:hover { 243 | background: #7d7d7d !important; 244 | } 245 | 246 | .history-pages { 247 | position: relative; 248 | display: flex; 249 | align-items: center; 250 | justify-content: center; 251 | column-gap: 0.5rem; 252 | padding: 0.5rem; 253 | 254 | width: 100%; 255 | background-color: rgba(0, 0, 0, 0.2); 256 | border-top: 1px solid rgba(0, 0, 0, 0.4); 257 | } 258 | 259 | .history-pages>button { 260 | position: relative; 261 | display: flex; 262 | align-items: center; 263 | justify-content: center; 264 | width: 1.5rem; 265 | height: 1.5rem; 266 | border: none; 267 | background-color: transparent; 268 | cursor: pointer; 269 | fill: #c9c9c9; 270 | transition: 0.1s; 271 | } 272 | 273 | .history-pages>button:hover { 274 | transform: scale(1.1); 275 | fill: white; 276 | } 277 | 278 | .gn-hidden { 279 | animation: fadeout 0.5s forwards ease-in-out; 280 | } 281 | 282 | .gn-showing { 283 | animation: fadein 0.5s forwards ease-in-out; 284 | } -------------------------------------------------------------------------------- /client/main.lua: -------------------------------------------------------------------------------- 1 | -- Type: the required type(s) for the argument 2 | -- Required: is the argument required 3 | -- Persistent: ignore argument if the notification is persistent 4 | local NOTI_TYPES = { 5 | style = { type = "string", required = true, persistent = false }, 6 | duration = { type = "number", required = false, persistent = true }, 7 | sound = { type = { "boolean", "table" }, required = false, persistent = false }, 8 | position = { type = "table", required = false, persistent = false }, 9 | image = { type = "string", required = false, persistent = false }, 10 | icon = { type = "string", required = false, persistent = false } 11 | } 12 | local PersistentNotiMap = {} 13 | local nuiReady 14 | 15 | -- Debug Print Notification 16 | local function DebugPrintInfo(data, persistent) 17 | if cfg.debugMode then 18 | print('Notification | Style: ' .. tostring(data.style) .. '\n | Title: ' .. tostring(data.title) .. '\n | Message: ' .. tostring(data.message) .. '\n | Image URL: ' .. tostring(data.image) .. '\n | Icon: ' .. tostring(data.icon) ..'\n | Duration: ' .. tostring(data.duration) .. '\n | Sound: ' .. tostring(data.sound) .. '\n | Custom: ' .. tostring(data.custom) .. '\n | Position: ' .. tostring(data.position) .. '\n | Persistent: ' .. tostring(persistent)) 19 | end 20 | end 21 | 22 | -- General Debug Print 23 | local function DebugPrint(msg) 24 | if cfg.debugMode then 25 | print(msg) 26 | end 27 | end 28 | 29 | local function printError(msg) 30 | local errMsg = ('^1[T-Notify Error] %s'):format(msg) 31 | print(errMsg) 32 | end 33 | 34 | local function checkTypes(argVal, types) 35 | for i = 1, #types do 36 | if type(argVal) == types[i] then 37 | return true 38 | end 39 | end 40 | 41 | return false 42 | end 43 | 44 | local function verifyTypes(notiTable, isPersistent) 45 | local usePersistent, notiVal, hasTypes 46 | for typeKey, v in pairs(NOTI_TYPES) do 47 | usePersistent = v.persistent and isPersistent or false 48 | notiVal = notiTable[typeKey] 49 | hasTypes = type(v.type) == 'table' and checkTypes(notiVal, v.type) or type(notiVal) == v.type 50 | 51 | if v.required then 52 | if not notiVal or not hasTypes then 53 | printError(('Invalid %s type for %s notification. Expected %s, got %s.'):format(typeKey, isPersistent and 'persistent' or 'non-persistent', type(v.type == 'table') and table.concat(v.type, ' or ') or v.type, type(notiVal))) 54 | return false 55 | end 56 | else 57 | if not isPersistent and notiVal and not hasTypes then 58 | printError(('Invalid %s type for %s notification. Expected %s, got %s.'):format(typeKey, isPersistent and 'persistent' or 'non-persistent', type(v.type == 'table') and table.concat(v.type, ' or ') or v.type, type(notiVal))) 59 | return false 60 | end 61 | end 62 | end 63 | return true 64 | end 65 | 66 | --Triggers a notification in the NUI using supplied params 67 | local function SendNotification(data) 68 | DebugPrintInfo(data) 69 | 70 | local areTypesValid = verifyTypes(data) 71 | 72 | if areTypesValid then 73 | SendNUIMessage(data) 74 | if type(sound) == 'table' then 75 | PlaySoundFrontend(-1, sound.name, sound.reference, 1) 76 | elseif sound == true then 77 | PlaySoundFrontend(-1, cfg.sound.name, cfg.sound.reference, 1) 78 | end 79 | end 80 | end 81 | 82 | --Triggers a notification using persistence 83 | local function SendPersistentNotification(step, id, options) 84 | if debugMode then 85 | print('PersistLog | ' ..'\nStep | ' .. step .. '\nID | ' .. id) 86 | end 87 | 88 | if not step or not id then 89 | return printError('Persistent notifications must have a valid step and id') 90 | end 91 | 92 | local areTypesValid = true 93 | 94 | if options then 95 | DebugPrintInfo(options, step .. ' ID: ' .. id) 96 | areTypesValid = verifyTypes(options, true) 97 | if type(options.sound) == 'table' then 98 | PlaySoundFrontend(-1, options.sound.name, options.sound.reference, 1) 99 | elseif options.sound == true then 100 | PlaySoundFrontend(-1, cfg.sound.name, cfg.sound.reference, 1) 101 | end 102 | end 103 | 104 | if step == 'start' then 105 | PersistentNotiMap[id] = true 106 | elseif step == 'end' then 107 | PersistentNotiMap[id] = nil 108 | end 109 | 110 | if areTypesValid then 111 | SendNUIMessage({ 112 | type = 'persistNoti', 113 | step = step, 114 | id = id, 115 | options = options 116 | }) 117 | end 118 | end 119 | 120 | --Initialize's Config after activated by Thread 121 | local function InitConfig() 122 | local initObject = { 123 | type = 'init', 124 | position = cfg.position, 125 | insertAnim = cfg.animations.insertAnimation, 126 | insertDuration = cfg.animations.insertDuration, 127 | removeAnim = cfg.animations.removeAnimation, 128 | removeDuration = cfg.animations.removeDuration, 129 | maxNotifications = cfg.maxNotifications, 130 | useHistory = cfg.useHistory, 131 | historyPosition = cfg.historyPosition, 132 | } 133 | DebugPrint('Sending Init Config: \n' .. json.encode(initObject)) 134 | SendNUIMessage(initObject) 135 | end 136 | 137 | RegisterNUICallback('nuiReady', function(_, cb) 138 | DebugPrint('NUI frame ready') 139 | nuiReady = true 140 | -- Send Config File after NUI frame ready 141 | InitConfig() 142 | cb({}) 143 | end) 144 | 145 | --OBJECT STYLED EXPORTS 146 | function Alert(data) 147 | SendNotification({ 148 | type = 'noti', 149 | style = data.style, 150 | duration = data.duration, 151 | title = nil, 152 | message = data.message, 153 | image = nil, 154 | sound = data.sound, 155 | custom = data.custom, 156 | position = data.position, 157 | icon = data.icon 158 | }) 159 | end 160 | 161 | exports('Alert', Alert) 162 | 163 | function Custom(data) 164 | SendNotification({ 165 | type = 'noti', 166 | style = data.style, 167 | duration = data.duration, 168 | title = data.title, 169 | message = data.message, 170 | image = data.image, 171 | sound = data.sound, 172 | custom = data.custom, 173 | position = data.position, 174 | icon = data.icon 175 | }) 176 | end 177 | 178 | exports('Custom', Custom) 179 | 180 | function Image(data) 181 | SendNotification({ 182 | type = 'noti', 183 | style = data.style, 184 | duration = data.duration, 185 | title = data.title, 186 | message = nil, 187 | image = data.image, 188 | sound = data.sound, 189 | custom = data.custom, 190 | position = data.position, 191 | icon = nil 192 | }) 193 | end 194 | 195 | exports('Image', Image) 196 | 197 | function Icon(data) 198 | SendNotification({ 199 | type = 'noti', 200 | style = data.style, 201 | duration = data.duration, 202 | title = data.title, 203 | message = data.message, 204 | image = nil, 205 | sound = data.sound, 206 | custom = data.custom, 207 | position = data.position, 208 | icon = data.icon 209 | }) 210 | end 211 | 212 | exports('Icon', Icon) 213 | 214 | function Persist(data) 215 | SendPersistentNotification(data.step, data.id, data.options) 216 | end 217 | 218 | exports('Persist', Persist) 219 | 220 | function IsPersistentShowing(id) 221 | return PersistentNotiMap[id] or false 222 | end 223 | 224 | exports('IsPersistentShowing', IsPersistentShowing) 225 | 226 | --Event Handlers from Server (Objects) 227 | RegisterNetEvent('t-notify:client:Alert', Alert) 228 | 229 | RegisterNetEvent('t-notify:client:Custom', Custom) 230 | 231 | RegisterNetEvent('t-notify:client:Image', Image) 232 | 233 | RegisterNetEvent('t-notify:client:Icon', Icon) 234 | 235 | RegisterNetEvent('t-notify:client:Persist', Persist) -------------------------------------------------------------------------------- /nui/assets/script.js: -------------------------------------------------------------------------------- 1 | // Global default variables 2 | import UseHistory from "./useHistory.js"; 3 | import {isBrowserEnv} from "./utils.js"; 4 | import {registerWindowDebug} from "./test.js"; 5 | 6 | let insertAnim; 7 | let insertDuration; 8 | let removeAnim; 9 | let removeDuration; 10 | let position; 11 | let maxNotifications; 12 | let notiHistory; 13 | 14 | // This is where we store persistent noti's 15 | const persistentNotis = new Map(); 16 | const RESOURCE_NAME = !isBrowserEnv() ? window.GetParentResourceName() : 't-notify' 17 | 18 | /** 19 | * @typedef NotiObject 20 | * @type {object} 21 | * @property {string} type - Type of notification 22 | * @property {string} style - Style of notification 23 | * @property {string} message - Message 24 | * @property {string} title - Title of message 25 | * @property {string} image - Image URL 26 | * @property {string} icon - FontAwesome Icon Class 27 | * @property {boolean} custom - Custom style 28 | * @property {string} position - Position 29 | * @property {number} duration - Time in ms 30 | */ 31 | 32 | window.addEventListener("message", (event) => { 33 | switch (event.data.type) { 34 | case "init": 35 | return initFunction(event.data); 36 | case "persistNoti": 37 | return playPersistentNoti(event.data); 38 | case "noti": 39 | return playNotification(event.data) 40 | case "history": 41 | return notiHistory.setHistoryVisibility(event.data.visible); 42 | case "getHistory": 43 | return fetch(`https://${RESOURCE_NAME}/getHistory`, { 44 | method: "POST", 45 | body: JSON.stringify(notiHistory.getHistory()), 46 | }); 47 | case "clearHistory": 48 | return notiHistory.clearHistory(); 49 | case "removeHistoryNoti": 50 | return notiHistory.removeNotificationById(event.data.id); 51 | } 52 | }); 53 | 54 | window.addEventListener("load", () => { 55 | if (isBrowserEnv()) return; 56 | fetch(`https://${RESOURCE_NAME}/nuiReady`, { 57 | method: "POST", 58 | }).catch((e) => console.error("Unable to send NUI ready message", e)); 59 | }); 60 | 61 | /** 62 | * @typedef InitData 63 | * @type {object} 64 | * @property {string} position - Position for notification 65 | * @property {string} insertAnim - Which insert animation to use 66 | * @property {number} insertDuration - Insert duration to use 67 | * @property {string} removeAnim - Which remove animation to use 68 | * @property {number} removeDuration - Remove duration to use 69 | * @property {number} maxNotifications - Max number of notifications to use 70 | * @property {boolean} useHistory - Whether to use notification history 71 | * @property {string} historyPosition - Position for notification history 72 | */ 73 | 74 | /** 75 | * Initialize default global variables 76 | * @param data {InitData} 77 | */ 78 | function initFunction(data) { 79 | position = data.position; 80 | insertAnim = data.insertAnim; 81 | insertDuration = data.insertDuration; 82 | removeAnim = data.removeAnim; 83 | removeDuration = data.removeDuration; 84 | maxNotifications = data.maxNotifications; 85 | 86 | // Initialize notification history 87 | if (data.useHistory) { 88 | notiHistory = new UseHistory(data.historyPosition); 89 | window.addEventListener("keyup", keyHandler); 90 | } else { 91 | notiHistory = new UseHistory(data.historyPosition, false); 92 | document.querySelector('.history-wrapper').remove(); 93 | } 94 | } 95 | 96 | /** 97 | * Initialize default global variables 98 | * @param noti {NotiObject} 99 | */ 100 | 101 | const createOptions = (noti) => ({ 102 | // Unfortunately cannot use optional chaining as I think NUI is ES6 103 | duration: noti.duration || undefined, 104 | position: noti.position || position, 105 | maxNotifications: maxNotifications, 106 | insertAnimation: { 107 | name: insertAnim, 108 | duration: insertDuration, 109 | }, 110 | removeAnimation: { 111 | name: removeAnim, 112 | duration: removeDuration, 113 | }, 114 | closeOnClick: false, 115 | closeButton: false 116 | }); 117 | 118 | /** 119 | * Save a notification to history 120 | * @param noti {NotiObject}- Notification Object 121 | */ 122 | function saveToHistory (noti) { 123 | if (notiHistory) notiHistory.addNotification(noti); 124 | } 125 | 126 | function keyHandler(e) { 127 | if (e.key === "Escape") { 128 | fetch(`https://${RESOURCE_NAME}/historyClose`).then((resp) => { 129 | if (resp) { 130 | notiHistory.setHistoryVisibility(false); 131 | } 132 | }).catch((e) => console.error("Unable to close history", e)); 133 | } 134 | } 135 | 136 | //Notification Function 137 | /** 138 | * Play a regular notification 139 | * @param noti {NotiObject} - Notification 140 | */ 141 | export function playNotification(noti) { 142 | // Sanity check 143 | if (noti) { 144 | const options = createOptions(noti); 145 | 146 | const content = { 147 | title: noti.title && noti.title.toString(), 148 | image: noti.image, 149 | icon: noti.icon, 150 | text: noti.message && noti.message.toString(), 151 | }; 152 | 153 | if (noti.custom) { 154 | const customClass = "gn-" + noti.style; 155 | SimpleNotification.custom([customClass], content, options); 156 | return; 157 | } 158 | 159 | 160 | SimpleNotification[noti.style.toLowerCase()](content, options); 161 | saveToHistory(noti); 162 | } 163 | } 164 | 165 | /** 166 | * 167 | * @param id {string} - Notification ID 168 | * @param noti {NotiObject}- Notification Object 169 | * @returns {void} 170 | */ 171 | const startPersistentNoti = (id, noti) => { 172 | if (persistentNotis.has(id)) 173 | return console.log( 174 | `Persistent Notification with that ID already exists (${id})` 175 | ); 176 | 177 | // Base options 178 | const options = createOptions(noti); 179 | 180 | // Add sticky property 181 | const persistOptions = { ...options, sticky: true }; 182 | 183 | // Create content object 184 | const content = { 185 | title: noti.title, 186 | image: noti.image, 187 | icon: noti.icon, 188 | text: noti.message, 189 | }; 190 | 191 | // Handle custom styling 192 | if (noti.custom) { 193 | // Auto prepend gn class 194 | const customClass = "gn-" + noti.style; 195 | 196 | persistentNotis.set( 197 | id, 198 | SimpleNotification.custom([customClass], content, persistOptions) 199 | ); 200 | saveToHistory(noti); 201 | return; 202 | } 203 | 204 | persistentNotis.set( 205 | id, 206 | SimpleNotification[noti.style.toLowerCase()](content, persistOptions) 207 | ); 208 | saveToHistory(noti); 209 | }; 210 | 211 | /** 212 | * End a persistent notification 213 | * @param id {string} - Persistent Notification ID 214 | * @returns {void} 215 | */ 216 | const endPersistentNoti = (id) => { 217 | if (!persistentNotis.has(id)) { 218 | console.error( 219 | "Persistent Notification ID not found in cache. First start a persistent notification before ending." 220 | ); 221 | return; 222 | } 223 | const noti = persistentNotis.get(id); 224 | noti.closeAnimated(); 225 | persistentNotis.delete(id); 226 | }; 227 | 228 | /** 229 | * Update a persistent notification 230 | * @param id {string} - Persistent Notification ID 231 | * @param noti {NotiObject}- Notification Object 232 | * @returns {void} 233 | */ 234 | const updatePersistentNoti = (id, noti) => { 235 | if (!persistentNotis.has(id)) { 236 | console.error( 237 | "Persistent Notification ID not found in cache. First start a persistent notification before updating." 238 | ); 239 | return; 240 | } 241 | 242 | const persistentNoti = persistentNotis.get(id); 243 | if (noti.image) { 244 | persistentNoti.setImage(noti.image) 245 | } 246 | 247 | if (noti.icon) { 248 | persistentNoti.setIcon(noti.icon) 249 | } 250 | 251 | if (noti.message) { 252 | persistentNoti.setText(noti.message) 253 | } 254 | 255 | if (noti.title) { 256 | persistentNoti.setTitle(noti.title) 257 | } 258 | }; 259 | 260 | /** 261 | * @typedef PersistentNoti 262 | * @type {object} 263 | * @property {string} type - Type of notification 264 | * @property {NotiObject} options - Type of notification 265 | * @property {string} step - Step for persistent noti 266 | * @property {string | number} id - Unique ID for persistent noti 267 | */ 268 | 269 | /** 270 | * Play a persistent notification 271 | * @param noti {PersistentNoti} - The persistent notification object 272 | */ 273 | function playPersistentNoti(noti) { 274 | const id = noti.id.toString(); 275 | 276 | switch (noti.step) { 277 | case "start": 278 | startPersistentNoti(id, noti.options); 279 | break; 280 | case "update": 281 | updatePersistentNoti(id, noti.options) 282 | break; 283 | case "end": 284 | endPersistentNoti(id); 285 | break; 286 | default: 287 | console.error( 288 | "Invalid step for persistent notification must be `start`, `end`, or `update`" 289 | ); 290 | } 291 | } 292 | 293 | // Lets register our debug methods for browser 294 | if (isBrowserEnv()) { 295 | registerWindowDebug(); 296 | notiHistory.setHistoryVisibility(true); 297 | } 298 | -------------------------------------------------------------------------------- /nui/SimpleNotification/notification.css: -------------------------------------------------------------------------------- 1 | @keyframes insert-left { 2 | from { 3 | transform: rotateY(-70deg); 4 | transform-origin: left; 5 | } 6 | 7 | to { 8 | transform: rotateY(0deg); 9 | transform-origin: left; 10 | } 11 | } 12 | 13 | @keyframes insert-top { 14 | from { 15 | transform: rotateX(70deg); 16 | transform-origin: top; 17 | } 18 | 19 | to { 20 | transform: rotateX(0deg); 21 | transform-origin: top; 22 | } 23 | } 24 | 25 | @keyframes insert-bottom { 26 | from { 27 | transform: rotateX(-70deg); 28 | transform-origin: bottom; 29 | } 30 | 31 | to { 32 | transform: rotateX(0deg); 33 | transform-origin: bottom; 34 | } 35 | } 36 | 37 | @keyframes insert-right { 38 | from { 39 | transform: rotateY(-70deg); 40 | transform-origin: right; 41 | } 42 | 43 | to { 44 | transform: rotateY(0deg); 45 | transform-origin: right; 46 | } 47 | } 48 | 49 | @keyframes fadein { 50 | from { 51 | opacity: 0; 52 | } 53 | 54 | to { 55 | opacity: 1; 56 | } 57 | } 58 | 59 | @keyframes fadeout { 60 | from { 61 | opacity: 1; 62 | } 63 | 64 | to { 65 | opacity: 0; 66 | } 67 | } 68 | 69 | @keyframes scalein { 70 | from { 71 | transform: scale(0); 72 | } 73 | 74 | to { 75 | transform: scale(1); 76 | } 77 | } 78 | 79 | @keyframes scaleout { 80 | from { 81 | transform: scale(1); 82 | } 83 | 84 | to { 85 | transform: scale(0); 86 | } 87 | } 88 | 89 | @keyframes rotatein { 90 | from { 91 | transform: rotate(0) scale(0); 92 | } 93 | 94 | to { 95 | transform: rotate(360deg) scale(1); 96 | } 97 | } 98 | 99 | @keyframes rotateout { 100 | from { 101 | transform: rotate(0) scale(1); 102 | } 103 | 104 | to { 105 | transform: rotate(-360deg) scale(0); 106 | } 107 | } 108 | 109 | @keyframes shorten { 110 | from { 111 | width: 100%; 112 | } 113 | 114 | to { 115 | width: 0; 116 | } 117 | } 118 | 119 | .gn-wrapper { 120 | position: fixed; 121 | pointer-events: none; 122 | display: flex; 123 | flex-direction: column; 124 | flex-wrap: nowrap; 125 | z-index: 1080; 126 | top: 0; 127 | left: 0; 128 | right: 0; 129 | bottom: 0; 130 | } 131 | 132 | .gn-top-right, 133 | .gn-bottom-right { 134 | align-items: flex-end; 135 | } 136 | 137 | .gn-bottom-right { 138 | justify-content: flex-end; 139 | } 140 | 141 | .gn-top-left, 142 | .gn-bottom-left { 143 | align-items: flex-start; 144 | } 145 | 146 | .gn-bottom-left { 147 | justify-content: flex-end; 148 | } 149 | 150 | .gn-top-center, 151 | .gn-bottom-center { 152 | align-items: center; 153 | } 154 | 155 | .gn-top-center { 156 | justify-content: flex-start; 157 | } 158 | 159 | .gn-bottom-center { 160 | justify-content: flex-end; 161 | } 162 | 163 | .gn-middle-left { 164 | justify-content: center; 165 | align-items: flex-start; 166 | flex-direction: column; 167 | } 168 | 169 | .gn-middle-right { 170 | justify-content: center; 171 | align-items: flex-end; 172 | flex-direction: column; 173 | } 174 | 175 | .gn-notification { 176 | flex-shrink: 0; 177 | font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif; 178 | box-shadow: 1px 1px 3px black; 179 | border-radius: 3px; 180 | overflow: hidden; 181 | margin: 1rem; 182 | cursor: default; 183 | pointer-events: all; 184 | min-width: 10rem; 185 | max-width: 25rem; 186 | position: relative; 187 | transition: background-color 0.2s ease-in-out; 188 | } 189 | 190 | .gn-insert { 191 | animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); 192 | animation-fill-mode: forwards; 193 | } 194 | 195 | .gn-close-on-click { 196 | cursor: pointer; 197 | } 198 | 199 | .gn-close { 200 | position: absolute; 201 | top: 0; 202 | right: 0; 203 | font-size: 1rem; 204 | padding: 0.5rem; 205 | border-left: 1px solid rgba(0, 0, 0, 0.4); 206 | border-bottom: 1px solid rgba(0, 0, 0, 0.4); 207 | border-bottom-left-radius: 3px; 208 | background: rgba(0, 0, 0, 0.2); 209 | opacity: 0; 210 | transition: all 100ms ease-in; 211 | cursor: pointer; 212 | user-select: none; 213 | -moz-user-select: none; 214 | } 215 | 216 | .gn-notification:hover .gn-close { 217 | opacity: 1; 218 | } 219 | 220 | .gn-close:hover { 221 | background: rgba(0, 0, 0, 0.6); 222 | } 223 | 224 | .gn-close.gn-close-title { 225 | display: flex; 226 | align-items: center; 227 | bottom: 0; 228 | border-bottom: 0; 229 | border-bottom-left-radius: 0; 230 | } 231 | 232 | .gn-notification>h1 { 233 | background-color: rgba(0, 0, 0, 0.2); 234 | border-bottom: 1px solid rgba(0, 0, 0, 0.4); 235 | overflow: hidden; 236 | text-overflow: ellipsis; 237 | position: relative; 238 | } 239 | 240 | .gn-remove { 241 | animation-timing-function: linear; 242 | animation-fill-mode: forwards; 243 | } 244 | 245 | .gn-lifespan { 246 | display: block; 247 | height: 3px; 248 | width: 100%; 249 | background-color: #4dd0e1; 250 | transition: height 0.4s ease-in-out, width 0s linear; 251 | } 252 | 253 | .gn-extinguish { 254 | animation-duration: 1000ms; 255 | animation-name: shorten; 256 | animation-timing-function: linear; 257 | animation-fill-mode: forwards; 258 | } 259 | 260 | .gn-lifespan.gn-retire { 261 | height: 0px; 262 | } 263 | 264 | .gn-success { 265 | background-color: #689f38; 266 | color: white; 267 | } 268 | 269 | .gn-info { 270 | background-color: #0288d1; 271 | color: white; 272 | } 273 | 274 | .gn-error { 275 | background-color: #b42f2d; 276 | color: white; 277 | } 278 | 279 | .gn-warning { 280 | background-color: #d87a00; 281 | color: white; 282 | } 283 | 284 | .gn-message { 285 | background-color: #333333; 286 | color: white; 287 | } 288 | 289 | .gn-content { 290 | display: flex; 291 | flex: 1; 292 | align-content: space-between; 293 | align-items: center; 294 | } 295 | 296 | .gn-content>img { 297 | max-width: 30%; 298 | max-height: 20rem; 299 | flex-shrink: 0; 300 | } 301 | 302 | .gn-content .gn-text { 303 | max-width: 100%; 304 | word-break: break-word; 305 | } 306 | 307 | .gn-content>img:only-child, 308 | .gn-content .gn-text:only-child { 309 | max-width: 100%; 310 | } 311 | 312 | .gn-notification>h1, 313 | .gn-content .gn-text { 314 | padding: 0.5rem; 315 | margin: 0; 316 | width: 100%; 317 | } 318 | 319 | .gn-content .gn-text a { 320 | color: rgba(255, 255, 255, 0.8); 321 | transition: all 0.2s ease-in-out; 322 | } 323 | 324 | .gn-content .gn-text a:hover { 325 | text-shadow: 1px 0 1px rgba(255, 255, 255, 0.8); 326 | border-radius: 2px; 327 | } 328 | 329 | .gn-content .gn-text h1, 330 | .gn-content .gn-text h2 { 331 | margin: 0.5rem 0; 332 | } 333 | 334 | .gn-content .gn-text h1 { 335 | font-size: 1.2rem; 336 | } 337 | 338 | .gn-content .gn-text h2 { 339 | font-size: 1.1rem; 340 | } 341 | 342 | .gn-content .gn-text img { 343 | height: auto; 344 | max-width: 100%; 345 | margin: 0.1rem 0; 346 | } 347 | 348 | .gn-bold { 349 | font-weight: bold; 350 | } 351 | 352 | .gn-italic { 353 | font-style: italic; 354 | } 355 | 356 | .gn-red { 357 | color: red; 358 | } 359 | 360 | .gn-green { 361 | color: green; 362 | } 363 | 364 | .gn-yellow { 365 | color: yellow; 366 | } 367 | 368 | .gn-blue { 369 | color: blue 370 | } 371 | 372 | .gn-cyan { 373 | color: cyan; 374 | } 375 | 376 | .gn-purple { 377 | color: purple; 378 | } 379 | 380 | .gn-orange { 381 | color: orange; 382 | } 383 | 384 | .gn-gray { 385 | color: gray; 386 | } 387 | 388 | .gn-white { 389 | color: white 390 | } 391 | 392 | .gn-code { 393 | font-family: SFMono-Regular, Menlo, 'Lucida Console', Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; 394 | padding: 0.1rem 0.2rem; 395 | background-color: #333333; 396 | color: #f7f7f7; 397 | line-height: 1.4; 398 | border-radius: 2px; 399 | box-shadow: 0 0 1px #333333; 400 | } 401 | 402 | .gn-message .gn-code { 403 | background-color: #4d4d4d; 404 | box-shadow: 0 0 1px #4d4d4d; 405 | } 406 | 407 | .gn-separator { 408 | display: block; 409 | width: 100%; 410 | border-bottom: 1px solid white; 411 | border-radius: 4px; 412 | height: 2px; 413 | line-height: 0px; 414 | margin: 0.75rem 0; 415 | } 416 | 417 | .gn-buttons { 418 | display: flex; 419 | flex-direction: row; 420 | flex-wrap: nowrap; 421 | justify-content: stretch; 422 | align-items: stretch; 423 | align-content: stretch; 424 | text-align: center; 425 | border-top: 1px solid rgba(0, 0, 0, 0.4); 426 | } 427 | 428 | .gn-button { 429 | width: 100%; 430 | padding: 0.5rem; 431 | border: 0; 432 | cursor: pointer; 433 | border-right: 1px solid rgba(0, 0, 0, 0.4); 434 | transition: all 0.1s ease-in; 435 | font-size: 1rem; 436 | } 437 | 438 | .gn-button:hover { 439 | background: rgba(0, 0, 0, 0.6); 440 | } 441 | 442 | .gn-button:disabled { 443 | background: rgba(0, 0, 0, 0.6); 444 | filter: grayscale(60%); 445 | } 446 | 447 | .gn-button:last-child { 448 | border-right: 0; 449 | } 450 | 451 | .gn-float-right { 452 | float: right; 453 | } 454 | 455 | .gn-text-icon { 456 | margin-left: 0.5rem; 457 | } 458 | 459 | .gn-title-icon { 460 | margin-right: 0.5rem; 461 | } -------------------------------------------------------------------------------- /nui/assets/useHistory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {NotiObject} HistoryObject 3 | * @property {string} id - Persistent Notification ID 4 | * @property {HTMLDivElement} el - Notification Element 5 | * 6 | */ 7 | 8 | /** 9 | * @type {string[]} 10 | */ 11 | const SEARCH_TYPES = [ 12 | 'title', 13 | 'message', 14 | 'style', 15 | 'date' 16 | ] 17 | 18 | class UseHistory { 19 | /** 20 | * Setup notification history 21 | * @param {string} position - The position of the history 22 | * @param {boolean} useHistory - Use history UI 23 | */ 24 | constructor(position, useHistory = true) { 25 | this.history = []; 26 | this.useHistory = useHistory; 27 | this.count = 0; 28 | if (!this.useHistory) { 29 | return; 30 | } 31 | this.activeFilter = null; 32 | this.filter = ''; 33 | this.maxNotis = 4; 34 | this.currentPage = 0; 35 | this.paginationEl = document.getElementById('history-pagination'); 36 | this.containerEl = document.getElementById('notification-history'); 37 | this.historyEl = document.querySelector('.history-container'); 38 | this.searchEl = document.querySelector('.history-search'); 39 | this.position = position; 40 | this.init(); 41 | } 42 | 43 | /** 44 | * Initialize the history content 45 | * by adding button listeners and 46 | * adding proper pagination 47 | */ 48 | init() { 49 | this.paginationEl.textContent = '1 / 1'; 50 | const leftBtn = document.getElementById('history-left'); 51 | const rightBtn = document.getElementById('history-right'); 52 | const searchBtn = document.getElementById('history-search'); 53 | const searchInput = document.getElementById('history-search-input'); 54 | document.querySelector('.history-wrapper').classList.add(`gn-${this.position}`); 55 | 56 | leftBtn.addEventListener('click', () => { 57 | const oldPage = this.currentPage; 58 | this.currentPage--; 59 | const useLength = this.activeFilter?.length ?? this.history.length; 60 | const maxPages = Math.ceil(useLength / this.maxNotis); 61 | if (this.currentPage < 0) { 62 | this.currentPage = maxPages - 1; 63 | } 64 | 65 | if (oldPage !== this.currentPage) { 66 | this.updateHistory(this.activeFilter ?? this.history); 67 | } 68 | }); 69 | 70 | rightBtn.addEventListener('click', () => { 71 | const oldPage = this.currentPage; 72 | this.currentPage++; 73 | const useLength = this.activeFilter?.length ?? this.history.length; 74 | const maxPages = Math.ceil(useLength / this.maxNotis); 75 | if (this.currentPage >= maxPages) { 76 | this.currentPage = 0; 77 | } 78 | 79 | if (oldPage !== this.currentPage) { 80 | this.updateHistory(this.activeFilter ?? this.history); 81 | } 82 | }); 83 | 84 | searchBtn.addEventListener('click', () => { 85 | this.searchHistory(searchInput.value); 86 | this.filter = searchInput.value; 87 | }); 88 | 89 | searchInput.addEventListener('keyup', (e) => { 90 | if (e.key === 'Enter') { 91 | this.searchHistory(searchInput.value); 92 | this.filter = searchInput.value; 93 | } 94 | }); 95 | 96 | this.showInfo(); 97 | } 98 | 99 | /** 100 | * Add a notification to the history 101 | * @param {Object} noti - The notification object 102 | * @param {string} noti.title - The notification title 103 | * @param {string} noti.message - The notification message 104 | * @param {string} noti.style - The notification style 105 | * @param {string} noti.icon - The notification icon 106 | */ 107 | addNotification(noti) { 108 | this.count++; 109 | const dateText = new Date().toLocaleTimeString(); 110 | if (!this.useHistory) { 111 | this.history.push({ 112 | id: `notification-${this.count}`, 113 | date: dateText, 114 | ...noti, 115 | }); 116 | return; 117 | } 118 | 119 | const { title, message, style, icon } = noti; 120 | const container = document.createElement('div'); 121 | const footer = document.createElement('div'); 122 | const titleEl = document.createElement('h2'); 123 | const messageEl = document.createElement('p'); 124 | const iconEl = this.createIcon(icon); 125 | const time = document.createElement('span'); 126 | const deleteBtn = document.createElement('button'); 127 | const date = new Date().toLocaleTimeString(); 128 | 129 | container.id = `notification-${this.count}`; 130 | container.classList.add(`gn-${style || 'info'}`); 131 | titleEl.textContent = title && title.length > 30 ? `${title.substring(0, 32)}...` : title || 'No title was provided'; 132 | message !== undefined ? this.getStringNode(message, messageEl) : messageEl.textContent = 'No message was provided'; 133 | deleteBtn.textContent = 'Delete'; 134 | 135 | container.classList.add('history-notification'); 136 | time.textContent = dateText; 137 | 138 | iconEl !== null && titleEl.prepend(iconEl); 139 | footer.appendChild(time); 140 | footer.appendChild(deleteBtn); 141 | container.append(titleEl, messageEl, footer); 142 | 143 | if (this.containerEl.children.length < this.maxNotis) { 144 | if (this.activeFilter) { 145 | // Append to container if the notification matches the filter 146 | if (this.filter) { 147 | if (this.filterNotification(this.filter, noti)) { 148 | this.containerEl.appendChild(container); 149 | } 150 | } 151 | } else { 152 | this.containerEl.appendChild(container); 153 | } 154 | } 155 | 156 | if (this.activeFilter) { 157 | this.activeFilter.push({ 158 | id: container.id, 159 | el: container, 160 | date: date, 161 | ...noti, 162 | }); 163 | } 164 | 165 | this.history.push({ 166 | id: container.id, 167 | el: container, 168 | date: date, 169 | ...noti, 170 | }); 171 | 172 | this.hideInfo(); 173 | this.updatePagination(this.activeFilter ?? this.history); 174 | 175 | deleteBtn.addEventListener('click', (e) => { 176 | this.removeNotification(e.target); 177 | }) 178 | } 179 | 180 | /** 181 | * Remove a notification from the history 182 | * by its target delete button element 183 | * @param {HTMLButtonElement} target 184 | */ 185 | removeNotification(target) { 186 | if (!this.useHistory) return; 187 | 188 | const parent = target.parentNode.parentNode; 189 | const idx = this.history.findIndex((noti) => noti.id === parent.id); 190 | this.history.splice(idx, 1); 191 | this.activeFilter?.splice(idx, 1); 192 | this.containerEl.removeChild(parent); 193 | this.showInfo(); 194 | this.updatePagination(this.activeFilter ?? this.history); 195 | 196 | // If the last notification is deleted on the current page, go to the previous page 197 | const useLength = this.activeFilter?.length ?? this.history.length; 198 | const maxPages = Math.ceil(useLength / this.maxNotis); 199 | if (useLength > 0 && useLength % this.maxNotis === 0 && this.currentPage >= maxPages) { 200 | this.currentPage--; 201 | this.updateHistory(this.activeFilter ?? this.history); 202 | } else { 203 | this.updatePage(this.activeFilter ?? this.history); 204 | } 205 | } 206 | 207 | /** 208 | * Removes a notification by its id 209 | * from the history only 210 | * @param {string} id 211 | */ 212 | removeNotificationById(id) { 213 | const notiId = `notification-${id}`; 214 | const idx = this.history.findIndex((noti) => noti.id === notiId); 215 | this.history.splice(idx, 1); 216 | } 217 | 218 | /** 219 | * Creates an icon element from a string 220 | * @param {string} icon - Font Awesome icon name 221 | * @returns {HTMLElement|null} 222 | */ 223 | createIcon(icon) { 224 | if (!icon) return null; 225 | const iconEl = document.createElement('i'); 226 | const classes = icon.split(' '); 227 | 228 | if (classes.length > 1) { 229 | iconEl.classList.add(...classes); 230 | } 231 | iconEl.classList.add('gn-title-icon'); 232 | return iconEl; 233 | } 234 | 235 | /** 236 | * Update the pagination text 237 | * based on the current page 238 | * and the total number of pages 239 | */ 240 | updatePagination(history = this.history) { 241 | if (!this.useHistory) return; 242 | 243 | if (history.length > 0) { 244 | const maxPages = Math.ceil(history.length / this.maxNotis); 245 | this.paginationEl.textContent = `${this.currentPage + 1} / ${maxPages}`; 246 | } 247 | } 248 | 249 | /** 250 | * Handles the update of the history 251 | * when a notification is deleted 252 | */ 253 | updatePage(history = this.history) { 254 | if (!this.useHistory) return; 255 | 256 | if (history.length > this.maxNotis) { 257 | const maxPages = Math.ceil(history / this.maxNotis); 258 | if (this.currentPage >= maxPages) { 259 | this.currentPage = maxPages - 1; 260 | } 261 | this.updateHistory(history); 262 | } 263 | } 264 | 265 | /** 266 | * Update the actual history content 267 | */ 268 | updateHistory(history = this.history) { 269 | if (!this.useHistory) return; 270 | 271 | this.containerEl.innerHTML = ''; 272 | const start = this.currentPage * this.maxNotis; 273 | const end = start + this.maxNotis; 274 | const newHistory = history.slice(start, end); 275 | newHistory.forEach((noti) => { 276 | this.containerEl.appendChild(noti.el); 277 | }); 278 | 279 | this.updatePagination(history); 280 | this.showInfo(); 281 | } 282 | 283 | getStringNode(str, node) { 284 | const keys = Object.keys(SimpleNotification.tags); 285 | // str = str.length > 85 ? str.substring(0, 85) + '...' : str; 286 | const finalNodes = []; 287 | for (let i = 0; i < keys.length; i++) { 288 | const { type, class: className, open, close } = SimpleNotification.tags[keys[i]]; 289 | 290 | // Loop through the string and find all the tags 291 | let openIdx = 0; 292 | let closeIdx = 0; 293 | let pastIdx = 0; 294 | let tempStr = str; 295 | while (tempStr.includes(open) && tempStr.includes(close)) { 296 | let tempText = ''; 297 | openIdx = tempStr.indexOf(open, pastIdx); 298 | closeIdx = tempStr.indexOf(close, openIdx + 1); 299 | if (openIdx === -1 || closeIdx === -1) break; 300 | tempText = tempStr.substring(openIdx + open.length, closeIdx); 301 | 302 | // Add the text before the tag 303 | if (openIdx > 0) { 304 | if (type !== 'a' && type !== 'h1' && type !== 'h1') { 305 | const newNode = document.createElement(type === 'img' ? 'button' : type); 306 | className && newNode.classList.add(className); 307 | type === 'img' && newNode.classList.add('history-img-btn'); 308 | type === 'img' ? newNode.textContent = 'IMAGE' : newNode.textContent = tempText; 309 | 310 | newNode.addEventListener('click', () => window.open(tempText, '_blank')); 311 | 312 | finalNodes.push({ 313 | nodeEl: newNode, 314 | openIdx: openIdx, 315 | closeIdx: closeIdx + close.length - 1, 316 | }); 317 | } else { 318 | const newNode = document.createElement('span'); 319 | newNode.textContent = tempText; 320 | finalNodes.push({ 321 | nodeEl: newNode, 322 | openIdx: openIdx, 323 | closeIdx: closeIdx + close.length - 1, 324 | }); 325 | } 326 | } 327 | pastIdx = closeIdx + close.length; 328 | } 329 | } 330 | 331 | if (finalNodes.length > 0) { 332 | finalNodes.sort((a, b) => a.openIdx - b.openIdx); 333 | let currentIdx = 0; 334 | let currentStrIdx = 0; 335 | let hasTextLeft = true; 336 | while (hasTextLeft) { 337 | const { nodeEl, openIdx, closeIdx } = finalNodes[currentIdx]; 338 | const beforeNodeText = document.createTextNode(str.substring(currentStrIdx, openIdx)); 339 | node.appendChild(beforeNodeText); 340 | node.appendChild(nodeEl); 341 | currentStrIdx = closeIdx + 1; 342 | currentIdx++; 343 | if (currentIdx >= finalNodes.length) { 344 | hasTextLeft = false; 345 | const afterNodeText = document.createTextNode(str.substring(currentStrIdx)); 346 | node.appendChild(afterNodeText); 347 | } 348 | } 349 | } else { 350 | node.textContent = str; 351 | } 352 | } 353 | 354 | /** 355 | * Filters through the history based on a filter 356 | * and updates the history content 357 | * @param {string} searchVal 358 | */ 359 | searchHistory(searchVal) { 360 | if (!this.useHistory) return; 361 | 362 | if (searchVal === '') { 363 | this.activeFilter = null; 364 | this.updateHistory(); 365 | return; 366 | } 367 | 368 | let tempHistory; 369 | let hasType = ''; 370 | for (const type of SEARCH_TYPES) { 371 | if (searchVal.includes(`${type}:`)) { 372 | hasType = type; 373 | break; 374 | } 375 | } 376 | 377 | if (hasType !== '') { 378 | tempHistory = this.history.filter((noti) => { 379 | return noti[hasType] && noti[hasType].toLowerCase().includes(searchVal.replace(`${hasType}:`, '').toLowerCase()); 380 | }); 381 | } else { 382 | tempHistory = this.history.filter((noti) => noti.title && noti.title.includes(searchVal) || noti.message && noti.message.includes(searchVal)); 383 | } 384 | 385 | this.currentPage = 0; 386 | this.activeFilter = tempHistory 387 | this.updateHistory(tempHistory); 388 | } 389 | 390 | /** 391 | * Remove the info element from the DOM 392 | */ 393 | hideInfo() { 394 | if (!this.useHistory) return; 395 | 396 | if (this.history.length > 0) { 397 | const infoEl = document.querySelector('.history-empty'); 398 | if (infoEl) { 399 | this.containerEl.removeChild(infoEl); 400 | } 401 | } 402 | } 403 | 404 | /** 405 | * Creates an info element and adds it to the DOM 406 | */ 407 | showInfo() { 408 | if (!this.useHistory) return; 409 | 410 | if (this.containerEl.children.length === 0) { 411 | const infoEl = document.createElement('p'); 412 | infoEl.textContent = 'No notifications'; 413 | infoEl.classList.add('history-empty'); 414 | this.containerEl.appendChild(infoEl); 415 | } 416 | } 417 | 418 | /** 419 | * Set the visibility of the history 420 | * @param {boolean} show - true to show, false to hide 421 | */ 422 | setHistoryVisibility(show) { 423 | if (!this.useHistory) return; 424 | 425 | const useAnim = show ? ['gn-showing', 'gn-hidden'] : ['gn-hidden', 'gn-showing']; 426 | 427 | if (show) { 428 | this.historyEl.style.display = 'flex'; 429 | this.searchEl.style.display = 'flex'; 430 | } 431 | 432 | if (this.historyEl.classList.contains(useAnim[1])) { 433 | this.historyEl.classList.remove(useAnim[1]); 434 | this.searchEl.classList.remove(useAnim[1]); 435 | } 436 | 437 | this.historyEl.classList.add(useAnim[0]); 438 | this.searchEl.classList.add(useAnim[0]); 439 | 440 | setTimeout(() => { 441 | if (show) { 442 | this.historyEl.classList.remove(useAnim[0]); 443 | this.searchEl.classList.remove(useAnim[0]); 444 | } else { 445 | this.historyEl.style.display = 'none'; 446 | this.searchEl.style.display = 'none'; 447 | } 448 | }, 500); 449 | } 450 | 451 | /** 452 | * Checks if the new notification has 453 | * the properties to pass the filter 454 | * @param {string} filter - The filter to check 455 | * @param {object} noti - Notification object 456 | * @returns {*|boolean|boolean} 457 | */ 458 | filterNotification(filter, noti) { 459 | if (filter === '') return true; 460 | 461 | let hasType = ''; 462 | for (const type of SEARCH_TYPES) { 463 | if (filter.includes(`${type}:`)) { 464 | hasType = type; 465 | break; 466 | } 467 | } 468 | 469 | if (hasType !== '') { 470 | return noti[hasType] && noti[hasType].toLowerCase().includes(filter.replace(`${hasType}:`, '').toLowerCase()); 471 | } else { 472 | return noti.title && noti.title.includes(filter) || noti.message && noti.message.includes(filter); 473 | } 474 | } 475 | 476 | /** 477 | * Returns the current noti history 478 | * @returns {Object[]} 479 | */ 480 | getHistory() { 481 | // Return the history without the DOM elements 482 | return this.history.map((noti) => { 483 | const { el, ...rest } = noti; 484 | return rest; 485 | }); 486 | } 487 | 488 | /** 489 | * Clears the noti history 490 | */ 491 | clearHistory() { 492 | this.history = []; 493 | } 494 | } 495 | 496 | export default UseHistory; -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Base Styling 2 | 3 | *These are used when passing the `style` property* 4 | 5 | **Default Styles** 6 | * `info` 7 | * `error` 8 | * `warning` 9 | * `success` 10 | * `message` 11 | 12 | T-Notify also allows for the addition of custom user-specified classes in the custom.css file that can be used in conjunction with the default styles. 13 | 14 | >By default, there is an example custom notification style included in `/nui/custom.css` that can be used as a reference to build upon. Below you can find a small guide referencing that class. 15 | 16 | ## Custom Classes Guide 17 | 18 | /* This snippet is taken from custom.css, in the 'nui' directory */ 19 | 20 | /* Always attempt to keep user edited CSS to this file only, unless you know what you are doing */ 21 | 22 | .gn-example { 23 | background-color: pink; 24 | color: black; 25 | text-shadow: 0 1px 1px white; 26 | } 27 | 28 | >This example above shows a custom style that can be invoked whenever a notification is sent. Custom styles **must** have their CSS class **always** prefixed by `gn-` otherwise they will not work correctly. 29 | 30 | ### Invoking a Custom Style 31 | 32 | In order to use a custom style when invoking a notification, the property ``custom`` ***MUST*** also be set to true. We'll use the example styling for this snippet: 33 | 34 | ```lua 35 | TriggerClientEvent('t-notify:client:Custom', source, { 36 | style = 'example', 37 | duration = 6000, 38 | title = 'Markdown Formatting Example', 39 | message = '``Code``\n **Bolded Text** \n *Italics Yo* \n # Header 1\n ## Header 2\n', 40 | sound = true, 41 | custom = true 42 | }) 43 | ``` 44 | 45 | *This snippet produces the following notification and is triggered server side:* 46 | 47 | ![Example Styled Notification](https://tasoagc.dev/u/0tiWNE.png) 48 | 49 | On the other hand, if we **forget** to set the `custom` property as **True** the following error will be produced: 50 | 51 | ```javascript 52 | Uncaught TypeError: SimpleNotification[noti.style] is not a function 53 | ``` 54 | 55 | ## Function Types 56 | 57 | >T-Notify has three main functions which can be used with either an `export` or a `TriggerClientEvent` 58 | 59 | **Alert** - *Send an alert styled notification with just a message, no title, no image.* 60 | 61 | **Custom** - *Send a custom notification according to any properties chosen by the user.* 62 | 63 | **Image** - *Send an image with an optional title.* 64 | 65 | **Persistent** - *Send a notification that is persistent* 66 | 67 | **Icon** - *Send a notification with a font-awesome supported icon* 68 | 69 | ## Triggering Notifications 70 | > In versions of T-Notify below v1.3.0, Client-Side exports were triggered a little differently. See the [deprecated](/deprecated) methods for more details. 71 | 72 | You can trigger notifications from both the Client-Side or the Server-Side. The object passed on either side has the exact same properties but an `export` is used on the Client-Side and a `TriggerClientEvent` is used on the Server-Side. 73 | 74 | Both of them require you pass an **Object**, here are some examples: 75 | 76 | **Client** 77 | ```lua 78 | exports['t-notify']:Alert({ 79 | style = 'error', 80 | message = 'Example alert from the client side' 81 | }) 82 | ``` 83 | **Server** 84 | ```lua 85 | TriggerClientEvent('t-notify:client:Custom', source, { 86 | style = 'info', 87 | title = 'Notification Example', 88 | message = 'Here is the message', 89 | duration = 5500 90 | }) 91 | ``` 92 | ### Object Properties 93 | Depending on the function, the object can have optional and required properties. The properties and their respective functions can be found below. 94 | 95 | * **Alert** 96 | * `style` {STRING} (Required) - One of the available styles as listed in the **[base styling](usage?id=base-styling)** section. 97 | * `message` {STRING} (Required) - Message to display in the alert. 98 | * `duration` {NUMBER} (Optional) - Duration to display notification in ms. *Defaults to 2500ms*. 99 | * `sound` {BOOL or OBJECT} (Optional) - If true, the notification will also have an alert sound. Can also accept an object for custom sound on a per notification basis. *Defaults to false*. 100 | * `name` {STRING} (Optional) - An audio name like what can be found in `config.lua` 101 | * `reference` {STRING} (Optional) - An audio reference like what can be found in `config.lua` 102 | * `custom` {BOOL} (Optional) - This ***must*** be set to true in order to utilize a custom style. *Defaults to false.* 103 | * `position` {STRING} (Optional) - Position of the notification to display (top-left, top-center, top-right, bottom-left, bottom-center, bottom-right, middle-left, middle-right) *Defaults to config* 104 | * **Custom** 105 | * `style` {STRING} (Required) - One of the available styles as listed in the **[base styling](usage?id=base-styling)** section. 106 | * `title` {STRING} (Optional) - Title to display in the notification. *Defaults to nil* 107 | * `message` {STRING} (Optional) - Message to display in the notification. *Defaults to nil* 108 | * `image` {STRING} (Optional) - Accepts an Image URL to embed into the notification. *Defaults to nil* 109 | * `duration` {NUMBER} (Optional) - Duration to display notification in ms. *Defaults to 2500ms*. 110 | * `sound` {BOOL or OBJECT} (Optional) - If true, the notification will also have an alert sound. Can also accept a table for custom sound on a per notification basis. *Defaults to false*. 111 | * `name` {STRING} (Optional) - An audio name like what can be found in `config.lua` 112 | * `reference` {STRING} (Optional) - An audio reference like what can be found in `config.lua` 113 | * `custom` {BOOL} (Optional) - This ***must*** be set to true in order to utilize a custom style that wasn't present by default. *Defaults to false*. 114 | * `position` {STRING} (Optional) - Position of the notification to display (top-left, top-center, top-right, bottom-left, bottom-center, bottom-right, middle-left, middle-right) *Defaults to config* 115 | * **Image** 116 | * `style` {STRING} (Required) - One of the available styles as listed in the **[base styling](usage?id=base-styling)** section. 117 | * `title` {STRING} (Optional) - Title to display in the notification. *Defaults to nil* 118 | * `image` {STRING} (Required) - Accepts an Image URL to embed into the notification 119 | * `duration` {NUMBER} (Optional) - Duration to display notification in ms. *Defaults to 2500ms*. 120 | * `sound` {BOOL or OBJECT} (Optional) - If true, the notification will also have an alert sound. Can also accept an object for custom sound on a per notification basis. *Defaults to false*. 121 | * `name` {STRING} (Optional) - An audio name like what can be found in `config.lua` 122 | * `reference` {STRING} (Optional) - An audio reference like what can be found in `config.lua` 123 | * `custom` {BOOL} (Optional) - This ***must*** be set to true in order to utilize a custom style that wasn't present by default. *Defaults to false*. 124 | * `position` {STRING} (Optional) - Position of the notification to display (top-left, top-center, top-right, bottom-left, bottom-center, bottom-right, middle-left, middle-right) *Defaults to config* 125 | * **Persistent** 126 | * `step` {STRING} (Required) - The specific step for the persistent notification call (start, update, end). 127 | * `id` {STRING} (Required) - The unique id for the persistent notification being called. This must be a unique id to each persistent notification. 128 | * `options` {OBJECT} (Optional) - Contains options for the notification. This object needs to be passed **when** a persistent notification is being called with the `'start'` step. 129 | * `style` {STRING} (Required) - One of the available styles as listed in the **[base styling](usage?id=base-styling)** section. 130 | * `title` {STRING} (Optional) - Title to display in the notification. *Defaults to nil* 131 | * `image` {STRING} (Optional) - Accepts an Image URL to embed into the notification 132 | * `sound` {BOOL or OBJECT} (Optional) - If true, the notification will also have an alert sound. Can also accept an object for custom sound on a per notification basis. *Defaults to false*. 133 | * `name` {STRING} (Optional) - An audio name like what can be found in `config.lua` 134 | * `reference` {STRING} (Optional) - An audio reference like what can be found in `config.lua` 135 | * `custom` {BOOL} (Optional) - This ***must*** be set to true in order to utilize a custom style that wasn't present by default. *Defaults to false*. 136 | * `position` {STRING} (Optional) - Position of the notification to display (top-left, top-center, top-right, bottom-left, bottom-center, bottom-right, middle-left, middle-right) *Defaults to config value* 137 | * **Icon** 138 | * `style` {STRING} (Required) - One of the available styles as listed in the 139 | **[base styling](usage?id=base-styling)** section. 140 | * `icon` {STRING} (Required) - Icon of the notification to display ([FontAwesome](https://fontawesome.com/v6.0/icons) supported icon). 141 | * `duration` {NUMBER} (Optional) - Duration to display notification in ms. *Defaults to 2500ms*. 142 | * `title` {STRING} (Optional) - Title to display in the notification. *Defaults to nil* 143 | * `message` {STRING} (Optional) - Message to display in the notification.*Defaults to nil* 144 | * `sound` {BOOL or OBJECT} (Optional) - If true, the notification will also have an alert sound. Can also accept an object for custom sound on a per notification basis. *Defaults to false*. 145 | * `name` {STRING} (Optional) - An audio name like what can be found in `config.lua` 146 | * `reference` {STRING} (Optional) - An audio reference like what can be found in `config.lua` 147 | * `custom` {BOOL} (Optional) - This ***must*** be set to true in order to utilize a custom style that wasn't present by default. *Defaults to false*. 148 | * `position` {STRING} (Optional) - Position of the notification to display (top-left, top-center, top-right, bottom-left, bottom-center, bottom-right) *Defaults to config* 149 | ### Examples 150 | 151 | Here are some example triggers for each of main functions. 152 | 153 | **Custom** 154 | ```lua 155 | -- Server-side 156 | TriggerClientEvent('t-notify:client:Custom', ServerID, { 157 | style = 'success', 158 | duration = 10500, 159 | title = 'Markdown Formatting Example', 160 | message = '``Code``\n **Bolded Text** \n *Italics Yo* \n # Header 1\n ## Header 2\n', 161 | sound = true 162 | }) 163 | 164 | -- Client-side 165 | exports['t-notify']:Custom({ 166 | style = 'success', 167 | duration = 10500, 168 | title = 'Markdown Formatting Example', 169 | message = '``Code``\n **Bolded Text** \n *Italics Yo* \n # Header 1\n ## Header 2\n', 170 | sound = true 171 | }) 172 | ``` 173 | This code snippet produced the following notification: 174 | 175 | ![Custom Example](https://tasoagc.dev/u/RyYTAX.png) 176 | 177 | **Alert** 178 | ```lua 179 | -- Server-side 180 | TriggerClientEvent('t-notify:client:Alert', ServerID, { 181 | style = 'error', 182 | message = '✔️ This is a success alert' 183 | }) 184 | 185 | -- Client-side 186 | exports['t-notify']:Alert({ 187 | style = 'success', 188 | message = '✔️ This is a success alert' 189 | }) 190 | ``` 191 | 192 | This code snippet produced the following notification: 193 | 194 | ![Alert Example](https://tasoagc.dev/u/WVzheO.png) 195 | 196 | **Image** 197 | ```lua 198 | -- Server-side 199 | TriggerClientEvent('t-notify:client:Image', ServerID, { 200 | style = 'info', 201 | duration = 11500, 202 | title = 'Notification with an Image', 203 | image = 'https://tasoagc.dev/u/61Gg0W.png', 204 | sound = true 205 | }) 206 | 207 | -- Client-side 208 | exports['t-notify']:Image({ 209 | style = 'info', 210 | duration = 11500, 211 | title = 'Notification with an Image', 212 | image = 'https://tasoagc.dev/u/61Gg0W.png', 213 | sound = true 214 | }) 215 | ``` 216 | This code snippet produced the following notification: 217 | 218 | ![Image Example](https://tasoagc.dev/u/wmcisu.png) 219 | 220 | **Persistent** 221 | 222 | *Starting a Persistent Notification:* 223 | 224 | ```lua 225 | -- Server-side 226 | TriggerClientEvent('t-notify:client:Persist', ServerID, { 227 | id = 'uniquePersistId', 228 | step = 'start', 229 | options = { 230 | style = 'info', 231 | title = 'Notification with an Image', 232 | image = 'https://tasoagc.dev/u/61Gg0W.png', 233 | sound = true 234 | } 235 | }) 236 | 237 | -- Client-side 238 | exports['t-notify']:Persist({ 239 | id = 'uniquePersistId', 240 | step = 'start', 241 | options = { 242 | style = 'info', 243 | title = 'Notification with an Image', 244 | image = 'https://tasoagc.dev/u/61Gg0W.png', 245 | sound = true 246 | } 247 | }) 248 | ``` 249 | 250 | *Updating a Persistent Notification:* 251 | 252 | ```lua 253 | -- Server-side 254 | TriggerClientEvent('t-notify:client:Persist', ServerID, { 255 | id = 'uniquePersistId', 256 | step = 'update', 257 | options = { 258 | style = 'info', 259 | title = 'Notification with an Image', 260 | image = 'https://tasoagc.dev/u/61Gg0W.png', 261 | message = 'This is a message' 262 | } 263 | }) 264 | 265 | -- Client-side 266 | exports['t-notify']:Persist({ 267 | id = 'uniquePersistId', 268 | step = 'update', 269 | options = { 270 | style = 'info', 271 | title = 'Notification with an Image', 272 | image = 'https://tasoagc.dev/u/61Gg0W.png', 273 | message = 'This is a message' 274 | } 275 | }) 276 | ``` 277 | 278 | *Ending a Persistent Notification:* 279 | ```lua 280 | -- Server-side 281 | TriggerClientEvent('t-notify:client:Persist', ServerID, { 282 | id = 'uniquePersistId', 283 | step = 'end' 284 | }) 285 | 286 | -- Client-side 287 | exports['t-notify']:Persist({ 288 | id = 'uniquePersistId', 289 | step = 'end' 290 | }) 291 | ``` 292 | 293 | *Getting a Persistent Notification:* 294 | ```lua 295 | -- Client-side 296 | -- Returns a boolean depending on whether the notification exists or not. 297 | local exists = exports['t-notify']:IsPersistentShowing('dead') 298 | ``` 299 | 300 | **Icon** 301 | ```lua 302 | -- Server-side 303 | TriggerClientEvent('t-notify:client:Icon', ServerID, { 304 | style = 'info', 305 | duration = 11500, 306 | message = 'Notification with an Icon', 307 | icon = 'fas fa-sign-out-alt', 308 | sound = true 309 | }) 310 | -- Client-side 311 | exports['t-notify']:Icon({ 312 | style = 'info', 313 | duration = 11500, 314 | title = 'Notification with an Icon and Title', 315 | message = 'Notification with an Icon', 316 | icon = 'fas fa-sign-out-alt fa-2xl', 317 | sound = true 318 | }) 319 | 320 | -- Use the following for custom sizing: https://fontawesome.com/docs/web/style/size 321 | ``` 322 | 323 | These code snippets produced the following notifications: 324 | 325 | ![Image Example](https://imgur.com/qDdqjLy.png) 326 | 327 | ![Image Example](https://imgur.com/Y1qZL7o.png) 328 | 329 | ## Markdown Formatting Tags 330 | 331 | >Notifications allows for *Markdown-like* tags to be used within the `message` property, allowing for easy text styling. Many of these tags can be nested to combine Markdown effects. 332 | 333 | | Name | Description | 334 | |---|---| 335 | | Inline code | \`\`code\`\` | 336 | | Header (h1) | ``# Header 1\n`` | 337 | | Header (h2) | ``## Header 2\n`` | 338 | | Link | ``{{title\|http://www.example.org/}}`` or ``{{http://www.example.org/}}`` without title. | 339 | | Image | ``![title\|http://www.example.org/image.jpg]`` or ``![http://www.example.org/image.jpg]`` without title. | 340 | | Bold | ``**http://www.example.org/**`` | 341 | | Italic | ``*http://www.example.org/*`` | 342 | | Separator | ``\n---\n`` | 343 | | Float right | ``>>Text<`` | 344 | 345 | **Example Code** 346 | 347 | Here's an example on how to use Markdown text in a notification called from the **server** 348 | 349 | ``` lua 350 | TriggerClientEvent('t-notify:client:Custom', ServerID, { 351 | style = 'success', 352 | duration = 10500, 353 | title = 'Markdown Formatting Example', 354 | message = '``Code``\n **Bolded Text** \n *Italics Yo* \n # Header 1\n ## Header 2\n', 355 | sound = true 356 | }) 357 | ``` 358 | 359 | This code snippet produced the following notification: 360 | 361 | ![Markdown Example](https://tasoagc.dev/u/RyYTAX.png) 362 | 363 | ## Color Formatting 364 | 365 | >With v1.4.0, there was a new addition to t-notify. With this new update, you are now capable of using colors you notification. You can begin using them by adding ~~ before and after your displayed message. 366 | 367 | | Code | Colors | 368 | |---|---| 369 | | r | Red | 370 | | g | Green | 371 | | y | Yellow | 372 | | b | Blue | 373 | | c | Cyan | 374 | | p | Purple | 375 | | w | White | 376 | | o | Orange | 377 | | gy | Gray | 378 | 379 | **Example Code** 380 | 381 | Here's an example on how to use colored text in a notification called from the **client** 382 | 383 | ``` lua 384 | exports['t-notify']:Custom({ 385 | style = 'message', 386 | duration = 11000, 387 | title = 'Colors Example', 388 | message = '~r~Red~r~ \n ~g~Green~g~ \n ~y~Yellow~y~ \n ~b~Blue~b~ \n ~c~Cyan~c~ \n ~p~Purple~p~ \n ~w~White~w~ \n ~o~Orange~o~ \n ~gy~Grey~gy~ \n', 389 | sound = true 390 | }) 391 | ``` 392 | 393 | This code snippet produced the following notification: 394 | 395 | ![Colors Example](https://camo.githubusercontent.com/f03940f6150420145ef63d5b82a6eaa0ec7ed65f0407c126088cb6f207be0b09/68747470733a2f2f692e7461736f6167632e6465762f42786b77) 396 | 397 | 398 | ## History Usage 399 | 400 | >With v2.1.0, there was a new addition to t-notify. With this new update, you are now capable of using t-notify's history or creating your own. You can find more information below. 401 | 402 | **Activating History** 403 | 404 | Go to your `config.lua` file and change `useHistory` to `true`. You can also change `historyPosition` to any position you want the history to be in. The default is `middle-right` which is the recommended position. 405 | 406 | **Setting History Visibility** 407 | 408 | If the history is enabled, you can set the visibility of the history along with NUI focus with code snippet below. 409 | 410 | ```lua 411 | local history = false 412 | RegisterCommand('history', function() 413 | history = not history 414 | exports['t-notify']:SetHistoryVisibility(history) -- Toggle history visibility 415 | end) 416 | ``` 417 | 418 | **Getting History Visibility** 419 | 420 | If the history is enabled, you can get the visibility of the history with the code snippet below. 421 | 422 | ```lua 423 | exports['t-notify']:GetHistoryVisibility() 424 | ``` 425 | 426 | **Creating Your Own History** 427 | 428 | With this new update, we also provide access to history. Check the following code snippet to see how to get the history. 429 | 430 | ```lua 431 | exports['t-notify']:GetHistory(function(history) 432 | print(history) -- Prints notification object + id + date (as an hour) 433 | SendNUIMessage({ 434 | action = 'history', -- Action to send to the NUI 435 | history = history -- History object 436 | }) 437 | end) 438 | ``` 439 | 440 | **Clearing History** 441 | 442 | The following export allows you to clear the history of notifications. 443 | 444 | ```lua 445 | exports['t-notify']:ClearHistory() -- Clears the history 446 | 447 | local newHistory = exports['t-notify']:GetHistory() -- Gets the new history 448 | print(json.encode(newHistory)) -- Returns [] 449 | ``` 450 | 451 | **Remove a notification from history** 452 | 453 | The following export allows you to remove a notification by its id from the history. Only need to pass the number of the id as it is formatted into `notification-yourId`. 454 | 455 | ```lua 456 | exports['t-notify']:RemoveFromHistory(1) -- Removes the notification from the history 457 | ``` 458 | 459 | ## History Search System 460 | >Along with the history system, we also provide a search system with the history UI. Below you will find information on how to use it. 461 | 462 | **Filtering History** 463 | There are 2 ways of filtering the history. In either case, you will need to input some text in the search bar. 464 | - Pressing `Enter` will filter the history by the text you inputted when you have your cursor in the search bar. 465 | - Pressing the `Search` button will filter the history by the text you inputted. 466 | 467 | **Using types** 468 | We can use types to filter notifications. By using `type:Filter`, we can use a filter instead of searching for a notification. The following types are available: 469 | 470 | - `title` - Filter by title 471 | - Example: `title:911` - Will filter all notifications with the title `911` 472 | - `message` - Filter by message 473 | - Example: `message:Hello` - Will filter all notifications that have the word `hello` in their message. 474 | - `style` - Filter by style 475 | - Example: `style:success` will filter all notifications with the style `success` 476 | - `date` - Filter by date 477 | - Example: `date:AM` will filter all notifications that were sent in the morning. -------------------------------------------------------------------------------- /docs/license-text.md: -------------------------------------------------------------------------------- 1 | ## Project License 2 | 3 | ``` 4 | GNU GENERAL PUBLIC LICENSE 5 | Version 3, 29 June 2007 6 | 7 | Copyright (C) 2007 Free Software Foundation, Inc. 8 | Everyone is permitted to copy and distribute verbatim copies 9 | of this license document, but changing it is not allowed. 10 | 11 | Preamble 12 | 13 | The GNU General Public License is a free, copyleft license for 14 | software and other kinds of works. 15 | 16 | The licenses for most software and other practical works are designed 17 | to take away your freedom to share and change the works. By contrast, 18 | the GNU General Public License is intended to guarantee your freedom to 19 | share and change all versions of a program--to make sure it remains free 20 | software for all its users. We, the Free Software Foundation, use the 21 | GNU General Public License for most of our software; it applies also to 22 | any other work released this way by its authors. You can apply it to 23 | your programs, too. 24 | 25 | When we speak of free software, we are referring to freedom, not 26 | price. Our General Public Licenses are designed to make sure that you 27 | have the freedom to distribute copies of free software (and charge for 28 | them if you wish), that you receive source code or can get it if you 29 | want it, that you can change the software or use pieces of it in new 30 | free programs, and that you know you can do these things. 31 | 32 | To protect your rights, we need to prevent others from denying you 33 | these rights or asking you to surrender the rights. Therefore, you have 34 | certain responsibilities if you distribute copies of the software, or if 35 | you modify it: responsibilities to respect the freedom of others. 36 | 37 | For example, if you distribute copies of such a program, whether 38 | gratis or for a fee, you must pass on to the recipients the same 39 | freedoms that you received. You must make sure that they, too, receive 40 | or can get the source code. And you must show them these terms so they 41 | know their rights. 42 | 43 | Developers that use the GNU GPL protect your rights with two steps: 44 | (1) assert copyright on the software, and (2) offer you this License 45 | giving you legal permission to copy, distribute and/or modify it. 46 | 47 | For the developers' and authors' protection, the GPL clearly explains 48 | that there is no warranty for this free software. For both users' and 49 | authors' sake, the GPL requires that modified versions be marked as 50 | changed, so that their problems will not be attributed erroneously to 51 | authors of previous versions. 52 | 53 | Some devices are designed to deny users access to install or run 54 | modified versions of the software inside them, although the manufacturer 55 | can do so. This is fundamentally incompatible with the aim of 56 | protecting users' freedom to change the software. The systematic 57 | pattern of such abuse occurs in the area of products for individuals to 58 | use, which is precisely where it is most unacceptable. Therefore, we 59 | have designed this version of the GPL to prohibit the practice for those 60 | products. If such problems arise substantially in other domains, we 61 | stand ready to extend this provision to those domains in future versions 62 | of the GPL, as needed to protect the freedom of users. 63 | 64 | Finally, every program is threatened constantly by software patents. 65 | States should not allow patents to restrict development and use of 66 | software on general-purpose computers, but in those that do, we wish to 67 | avoid the special danger that patents applied to a free program could 68 | make it effectively proprietary. To prevent this, the GPL assures that 69 | patents cannot be used to render the program non-free. 70 | 71 | The precise terms and conditions for copying, distribution and 72 | modification follow. 73 | 74 | TERMS AND CONDITIONS 75 | 76 | 0. Definitions. 77 | 78 | "This License" refers to version 3 of the GNU General Public License. 79 | 80 | "Copyright" also means copyright-like laws that apply to other kinds of 81 | works, such as semiconductor masks. 82 | 83 | "The Program" refers to any copyrightable work licensed under this 84 | License. Each licensee is addressed as "you". "Licensees" and 85 | "recipients" may be individuals or organizations. 86 | 87 | To "modify" a work means to copy from or adapt all or part of the work 88 | in a fashion requiring copyright permission, other than the making of an 89 | exact copy. The resulting work is called a "modified version" of the 90 | earlier work or a work "based on" the earlier work. 91 | 92 | A "covered work" means either the unmodified Program or a work based 93 | on the Program. 94 | 95 | To "propagate" a work means to do anything with it that, without 96 | permission, would make you directly or secondarily liable for 97 | infringement under applicable copyright law, except executing it on a 98 | computer or modifying a private copy. Propagation includes copying, 99 | distribution (with or without modification), making available to the 100 | public, and in some countries other activities as well. 101 | 102 | To "convey" a work means any kind of propagation that enables other 103 | parties to make or receive copies. Mere interaction with a user through 104 | a computer network, with no transfer of a copy, is not conveying. 105 | 106 | An interactive user interface displays "Appropriate Legal Notices" 107 | to the extent that it includes a convenient and prominently visible 108 | feature that (1) displays an appropriate copyright notice, and (2) 109 | tells the user that there is no warranty for the work (except to the 110 | extent that warranties are provided), that licensees may convey the 111 | work under this License, and how to view a copy of this License. If 112 | the interface presents a list of user commands or options, such as a 113 | menu, a prominent item in the list meets this criterion. 114 | 115 | 1. Source Code. 116 | 117 | The "source code" for a work means the preferred form of the work 118 | for making modifications to it. "Object code" means any non-source 119 | form of a work. 120 | 121 | A "Standard Interface" means an interface that either is an official 122 | standard defined by a recognized standards body, or, in the case of 123 | interfaces specified for a particular programming language, one that 124 | is widely used among developers working in that language. 125 | 126 | The "System Libraries" of an executable work include anything, other 127 | than the work as a whole, that (a) is included in the normal form of 128 | packaging a Major Component, but which is not part of that Major 129 | Component, and (b) serves only to enable use of the work with that 130 | Major Component, or to implement a Standard Interface for which an 131 | implementation is available to the public in source code form. A 132 | "Major Component", in this context, means a major essential component 133 | (kernel, window system, and so on) of the specific operating system 134 | (if any) on which the executable work runs, or a compiler used to 135 | produce the work, or an object code interpreter used to run it. 136 | 137 | The "Corresponding Source" for a work in object code form means all 138 | the source code needed to generate, install, and (for an executable 139 | work) run the object code and to modify the work, including scripts to 140 | control those activities. However, it does not include the work's 141 | System Libraries, or general-purpose tools or generally available free 142 | programs which are used unmodified in performing those activities but 143 | which are not part of the work. For example, Corresponding Source 144 | includes interface definition files associated with source files for 145 | the work, and the source code for shared libraries and dynamically 146 | linked subprograms that the work is specifically designed to require, 147 | such as by intimate data communication or control flow between those 148 | subprograms and other parts of the work. 149 | 150 | The Corresponding Source need not include anything that users 151 | can regenerate automatically from other parts of the Corresponding 152 | Source. 153 | 154 | The Corresponding Source for a work in source code form is that 155 | same work. 156 | 157 | 2. Basic Permissions. 158 | 159 | All rights granted under this License are granted for the term of 160 | copyright on the Program, and are irrevocable provided the stated 161 | conditions are met. This License explicitly affirms your unlimited 162 | permission to run the unmodified Program. The output from running a 163 | covered work is covered by this License only if the output, given its 164 | content, constitutes a covered work. This License acknowledges your 165 | rights of fair use or other equivalent, as provided by copyright law. 166 | 167 | You may make, run and propagate covered works that you do not 168 | convey, without conditions so long as your license otherwise remains 169 | in force. You may convey covered works to others for the sole purpose 170 | of having them make modifications exclusively for you, or provide you 171 | with facilities for running those works, provided that you comply with 172 | the terms of this License in conveying all material for which you do 173 | not control copyright. Those thus making or running the covered works 174 | for you must do so exclusively on your behalf, under your direction 175 | and control, on terms that prohibit them from making any copies of 176 | your copyrighted material outside their relationship with you. 177 | 178 | Conveying under any other circumstances is permitted solely under 179 | the conditions stated below. Sublicensing is not allowed; section 10 180 | makes it unnecessary. 181 | 182 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 183 | 184 | No covered work shall be deemed part of an effective technological 185 | measure under any applicable law fulfilling obligations under article 186 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 187 | similar laws prohibiting or restricting circumvention of such 188 | measures. 189 | 190 | When you convey a covered work, you waive any legal power to forbid 191 | circumvention of technological measures to the extent such circumvention 192 | is effected by exercising rights under this License with respect to 193 | the covered work, and you disclaim any intention to limit operation or 194 | modification of the work as a means of enforcing, against the work's 195 | users, your or third parties' legal rights to forbid circumvention of 196 | technological measures. 197 | 198 | 4. Conveying Verbatim Copies. 199 | 200 | You may convey verbatim copies of the Program's source code as you 201 | receive it, in any medium, provided that you conspicuously and 202 | appropriately publish on each copy an appropriate copyright notice; 203 | keep intact all notices stating that this License and any 204 | non-permissive terms added in accord with section 7 apply to the code; 205 | keep intact all notices of the absence of any warranty; and give all 206 | recipients a copy of this License along with the Program. 207 | 208 | You may charge any price or no price for each copy that you convey, 209 | and you may offer support or warranty protection for a fee. 210 | 211 | 5. Conveying Modified Source Versions. 212 | 213 | You may convey a work based on the Program, or the modifications to 214 | produce it from the Program, in the form of source code under the 215 | terms of section 4, provided that you also meet all of these conditions: 216 | 217 | a) The work must carry prominent notices stating that you modified 218 | it, and giving a relevant date. 219 | 220 | b) The work must carry prominent notices stating that it is 221 | released under this License and any conditions added under section 222 | 7. This requirement modifies the requirement in section 4 to 223 | "keep intact all notices". 224 | 225 | c) You must license the entire work, as a whole, under this 226 | License to anyone who comes into possession of a copy. This 227 | License will therefore apply, along with any applicable section 7 228 | additional terms, to the whole of the work, and all its parts, 229 | regardless of how they are packaged. This License gives no 230 | permission to license the work in any other way, but it does not 231 | invalidate such permission if you have separately received it. 232 | 233 | d) If the work has interactive user interfaces, each must display 234 | Appropriate Legal Notices; however, if the Program has interactive 235 | interfaces that do not display Appropriate Legal Notices, your 236 | work need not make them do so. 237 | 238 | A compilation of a covered work with other separate and independent 239 | works, which are not by their nature extensions of the covered work, 240 | and which are not combined with it such as to form a larger program, 241 | in or on a volume of a storage or distribution medium, is called an 242 | "aggregate" if the compilation and its resulting copyright are not 243 | used to limit the access or legal rights of the compilation's users 244 | beyond what the individual works permit. Inclusion of a covered work 245 | in an aggregate does not cause this License to apply to the other 246 | parts of the aggregate. 247 | 248 | 6. Conveying Non-Source Forms. 249 | 250 | You may convey a covered work in object code form under the terms 251 | of sections 4 and 5, provided that you also convey the 252 | machine-readable Corresponding Source under the terms of this License, 253 | in one of these ways: 254 | 255 | a) Convey the object code in, or embodied in, a physical product 256 | (including a physical distribution medium), accompanied by the 257 | Corresponding Source fixed on a durable physical medium 258 | customarily used for software interchange. 259 | 260 | b) Convey the object code in, or embodied in, a physical product 261 | (including a physical distribution medium), accompanied by a 262 | written offer, valid for at least three years and valid for as 263 | long as you offer spare parts or customer support for that product 264 | model, to give anyone who possesses the object code either (1) a 265 | copy of the Corresponding Source for all the software in the 266 | product that is covered by this License, on a durable physical 267 | medium customarily used for software interchange, for a price no 268 | more than your reasonable cost of physically performing this 269 | conveying of source, or (2) access to copy the 270 | Corresponding Source from a network server at no charge. 271 | 272 | c) Convey individual copies of the object code with a copy of the 273 | written offer to provide the Corresponding Source. This 274 | alternative is allowed only occasionally and noncommercially, and 275 | only if you received the object code with such an offer, in accord 276 | with subsection 6b. 277 | 278 | d) Convey the object code by offering access from a designated 279 | place (gratis or for a charge), and offer equivalent access to the 280 | Corresponding Source in the same way through the same place at no 281 | further charge. You need not require recipients to copy the 282 | Corresponding Source along with the object code. If the place to 283 | copy the object code is a network server, the Corresponding Source 284 | may be on a different server (operated by you or a third party) 285 | that supports equivalent copying facilities, provided you maintain 286 | clear directions next to the object code saying where to find the 287 | Corresponding Source. Regardless of what server hosts the 288 | Corresponding Source, you remain obligated to ensure that it is 289 | available for as long as needed to satisfy these requirements. 290 | 291 | e) Convey the object code using peer-to-peer transmission, provided 292 | you inform other peers where the object code and Corresponding 293 | Source of the work are being offered to the general public at no 294 | charge under subsection 6d. 295 | 296 | A separable portion of the object code, whose source code is excluded 297 | from the Corresponding Source as a System Library, need not be 298 | included in conveying the object code work. 299 | 300 | A "User Product" is either (1) a "consumer product", which means any 301 | tangible personal property which is normally used for personal, family, 302 | or household purposes, or (2) anything designed or sold for incorporation 303 | into a dwelling. In determining whether a product is a consumer product, 304 | doubtful cases shall be resolved in favor of coverage. For a particular 305 | product received by a particular user, "normally used" refers to a 306 | typical or common use of that class of product, regardless of the status 307 | of the particular user or of the way in which the particular user 308 | actually uses, or expects or is expected to use, the product. A product 309 | is a consumer product regardless of whether the product has substantial 310 | commercial, industrial or non-consumer uses, unless such uses represent 311 | the only significant mode of use of the product. 312 | 313 | "Installation Information" for a User Product means any methods, 314 | procedures, authorization keys, or other information required to install 315 | and execute modified versions of a covered work in that User Product from 316 | a modified version of its Corresponding Source. The information must 317 | suffice to ensure that the continued functioning of the modified object 318 | code is in no case prevented or interfered with solely because 319 | modification has been made. 320 | 321 | If you convey an object code work under this section in, or with, or 322 | specifically for use in, a User Product, and the conveying occurs as 323 | part of a transaction in which the right of possession and use of the 324 | User Product is transferred to the recipient in perpetuity or for a 325 | fixed term (regardless of how the transaction is characterized), the 326 | Corresponding Source conveyed under this section must be accompanied 327 | by the Installation Information. But this requirement does not apply 328 | if neither you nor any third party retains the ability to install 329 | modified object code on the User Product (for example, the work has 330 | been installed in ROM). 331 | 332 | The requirement to provide Installation Information does not include a 333 | requirement to continue to provide support service, warranty, or updates 334 | for a work that has been modified or installed by the recipient, or for 335 | the User Product in which it has been modified or installed. Access to a 336 | network may be denied when the modification itself materially and 337 | adversely affects the operation of the network or violates the rules and 338 | protocols for communication across the network. 339 | 340 | Corresponding Source conveyed, and Installation Information provided, 341 | in accord with this section must be in a format that is publicly 342 | documented (and with an implementation available to the public in 343 | source code form), and must require no special password or key for 344 | unpacking, reading or copying. 345 | 346 | 7. Additional Terms. 347 | 348 | "Additional permissions" are terms that supplement the terms of this 349 | License by making exceptions from one or more of its conditions. 350 | Additional permissions that are applicable to the entire Program shall 351 | be treated as though they were included in this License, to the extent 352 | that they are valid under applicable law. If additional permissions 353 | apply only to part of the Program, that part may be used separately 354 | under those permissions, but the entire Program remains governed by 355 | this License without regard to the additional permissions. 356 | 357 | When you convey a copy of a covered work, you may at your option 358 | remove any additional permissions from that copy, or from any part of 359 | it. (Additional permissions may be written to require their own 360 | removal in certain cases when you modify the work.) You may place 361 | additional permissions on material, added by you to a covered work, 362 | for which you have or can give appropriate copyright permission. 363 | 364 | Notwithstanding any other provision of this License, for material you 365 | add to a covered work, you may (if authorized by the copyright holders of 366 | that material) supplement the terms of this License with terms: 367 | 368 | a) Disclaiming warranty or limiting liability differently from the 369 | terms of sections 15 and 16 of this License; or 370 | 371 | b) Requiring preservation of specified reasonable legal notices or 372 | author attributions in that material or in the Appropriate Legal 373 | Notices displayed by works containing it; or 374 | 375 | c) Prohibiting misrepresentation of the origin of that material, or 376 | requiring that modified versions of such material be marked in 377 | reasonable ways as different from the original version; or 378 | 379 | d) Limiting the use for publicity purposes of names of licensors or 380 | authors of the material; or 381 | 382 | e) Declining to grant rights under trademark law for use of some 383 | trade names, trademarks, or service marks; or 384 | 385 | f) Requiring indemnification of licensors and authors of that 386 | material by anyone who conveys the material (or modified versions of 387 | it) with contractual assumptions of liability to the recipient, for 388 | any liability that these contractual assumptions directly impose on 389 | those licensors and authors. 390 | 391 | All other non-permissive additional terms are considered "further 392 | restrictions" within the meaning of section 10. If the Program as you 393 | received it, or any part of it, contains a notice stating that it is 394 | governed by this License along with a term that is a further 395 | restriction, you may remove that term. If a license document contains 396 | a further restriction but permits relicensing or conveying under this 397 | License, you may add to a covered work material governed by the terms 398 | of that license document, provided that the further restriction does 399 | not survive such relicensing or conveying. 400 | 401 | If you add terms to a covered work in accord with this section, you 402 | must place, in the relevant source files, a statement of the 403 | additional terms that apply to those files, or a notice indicating 404 | where to find the applicable terms. 405 | 406 | Additional terms, permissive or non-permissive, may be stated in the 407 | form of a separately written license, or stated as exceptions; 408 | the above requirements apply either way. 409 | 410 | 8. Termination. 411 | 412 | You may not propagate or modify a covered work except as expressly 413 | provided under this License. Any attempt otherwise to propagate or 414 | modify it is void, and will automatically terminate your rights under 415 | this License (including any patent licenses granted under the third 416 | paragraph of section 11). 417 | 418 | However, if you cease all violation of this License, then your 419 | license from a particular copyright holder is reinstated (a) 420 | provisionally, unless and until the copyright holder explicitly and 421 | finally terminates your license, and (b) permanently, if the copyright 422 | holder fails to notify you of the violation by some reasonable means 423 | prior to 60 days after the cessation. 424 | 425 | Moreover, your license from a particular copyright holder is 426 | reinstated permanently if the copyright holder notifies you of the 427 | violation by some reasonable means, this is the first time you have 428 | received notice of violation of this License (for any work) from that 429 | copyright holder, and you cure the violation prior to 30 days after 430 | your receipt of the notice. 431 | 432 | Termination of your rights under this section does not terminate the 433 | licenses of parties who have received copies or rights from you under 434 | this License. If your rights have been terminated and not permanently 435 | reinstated, you do not qualify to receive new licenses for the same 436 | material under section 10. 437 | 438 | 9. Acceptance Not Required for Having Copies. 439 | 440 | You are not required to accept this License in order to receive or 441 | run a copy of the Program. Ancillary propagation of a covered work 442 | occurring solely as a consequence of using peer-to-peer transmission 443 | to receive a copy likewise does not require acceptance. However, 444 | nothing other than this License grants you permission to propagate or 445 | modify any covered work. These actions infringe copyright if you do 446 | not accept this License. Therefore, by modifying or propagating a 447 | covered work, you indicate your acceptance of this License to do so. 448 | 449 | 10. Automatic Licensing of Downstream Recipients. 450 | 451 | Each time you convey a covered work, the recipient automatically 452 | receives a license from the original licensors, to run, modify and 453 | propagate that work, subject to this License. You are not responsible 454 | for enforcing compliance by third parties with this License. 455 | 456 | An "entity transaction" is a transaction transferring control of an 457 | organization, or substantially all assets of one, or subdividing an 458 | organization, or merging organizations. If propagation of a covered 459 | work results from an entity transaction, each party to that 460 | transaction who receives a copy of the work also receives whatever 461 | licenses to the work the party's predecessor in interest had or could 462 | give under the previous paragraph, plus a right to possession of the 463 | Corresponding Source of the work from the predecessor in interest, if 464 | the predecessor has it or can get it with reasonable efforts. 465 | 466 | You may not impose any further restrictions on the exercise of the 467 | rights granted or affirmed under this License. For example, you may 468 | not impose a license fee, royalty, or other charge for exercise of 469 | rights granted under this License, and you may not initiate litigation 470 | (including a cross-claim or counterclaim in a lawsuit) alleging that 471 | any patent claim is infringed by making, using, selling, offering for 472 | sale, or importing the Program or any portion of it. 473 | 474 | 11. Patents. 475 | 476 | A "contributor" is a copyright holder who authorizes use under this 477 | License of the Program or a work on which the Program is based. The 478 | work thus licensed is called the contributor's "contributor version". 479 | 480 | A contributor's "essential patent claims" are all patent claims 481 | owned or controlled by the contributor, whether already acquired or 482 | hereafter acquired, that would be infringed by some manner, permitted 483 | by this License, of making, using, or selling its contributor version, 484 | but do not include claims that would be infringed only as a 485 | consequence of further modification of the contributor version. For 486 | purposes of this definition, "control" includes the right to grant 487 | patent sublicenses in a manner consistent with the requirements of 488 | this License. 489 | 490 | Each contributor grants you a non-exclusive, worldwide, royalty-free 491 | patent license under the contributor's essential patent claims, to 492 | make, use, sell, offer for sale, import and otherwise run, modify and 493 | propagate the contents of its contributor version. 494 | 495 | In the following three paragraphs, a "patent license" is any express 496 | agreement or commitment, however denominated, not to enforce a patent 497 | (such as an express permission to practice a patent or covenant not to 498 | sue for patent infringement). To "grant" such a patent license to a 499 | party means to make such an agreement or commitment not to enforce a 500 | patent against the party. 501 | 502 | If you convey a covered work, knowingly relying on a patent license, 503 | and the Corresponding Source of the work is not available for anyone 504 | to copy, free of charge and under the terms of this License, through a 505 | publicly available network server or other readily accessible means, 506 | then you must either (1) cause the Corresponding Source to be so 507 | available, or (2) arrange to deprive yourself of the benefit of the 508 | patent license for this particular work, or (3) arrange, in a manner 509 | consistent with the requirements of this License, to extend the patent 510 | license to downstream recipients. "Knowingly relying" means you have 511 | actual knowledge that, but for the patent license, your conveying the 512 | covered work in a country, or your recipient's use of the covered work 513 | in a country, would infringe one or more identifiable patents in that 514 | country that you have reason to believe are valid. 515 | 516 | If, pursuant to or in connection with a single transaction or 517 | arrangement, you convey, or propagate by procuring conveyance of, a 518 | covered work, and grant a patent license to some of the parties 519 | receiving the covered work authorizing them to use, propagate, modify 520 | or convey a specific copy of the covered work, then the patent license 521 | you grant is automatically extended to all recipients of the covered 522 | work and works based on it. 523 | 524 | A patent license is "discriminatory" if it does not include within 525 | the scope of its coverage, prohibits the exercise of, or is 526 | conditioned on the non-exercise of one or more of the rights that are 527 | specifically granted under this License. You may not convey a covered 528 | work if you are a party to an arrangement with a third party that is 529 | in the business of distributing software, under which you make payment 530 | to the third party based on the extent of your activity of conveying 531 | the work, and under which the third party grants, to any of the 532 | parties who would receive the covered work from you, a discriminatory 533 | patent license (a) in connection with copies of the covered work 534 | conveyed by you (or copies made from those copies), or (b) primarily 535 | for and in connection with specific products or compilations that 536 | contain the covered work, unless you entered into that arrangement, 537 | or that patent license was granted, prior to 28 March 2007. 538 | 539 | Nothing in this License shall be construed as excluding or limiting 540 | any implied license or other defenses to infringement that may 541 | otherwise be available to you under applicable patent law. 542 | 543 | 12. No Surrender of Others' Freedom. 544 | 545 | If conditions are imposed on you (whether by court order, agreement or 546 | otherwise) that contradict the conditions of this License, they do not 547 | excuse you from the conditions of this License. If you cannot convey a 548 | covered work so as to satisfy simultaneously your obligations under this 549 | License and any other pertinent obligations, then as a consequence you may 550 | not convey it at all. For example, if you agree to terms that obligate you 551 | to collect a royalty for further conveying from those to whom you convey 552 | the Program, the only way you could satisfy both those terms and this 553 | License would be to refrain entirely from conveying the Program. 554 | 555 | 13. Use with the GNU Affero General Public License. 556 | 557 | Notwithstanding any other provision of this License, you have 558 | permission to link or combine any covered work with a work licensed 559 | under version 3 of the GNU Affero General Public License into a single 560 | combined work, and to convey the resulting work. The terms of this 561 | License will continue to apply to the part which is the covered work, 562 | but the special requirements of the GNU Affero General Public License, 563 | section 13, concerning interaction through a network will apply to the 564 | combination as such. 565 | 566 | 14. Revised Versions of this License. 567 | 568 | The Free Software Foundation may publish revised and/or new versions of 569 | the GNU General Public License from time to time. Such new versions will 570 | be similar in spirit to the present version, but may differ in detail to 571 | address new problems or concerns. 572 | 573 | Each version is given a distinguishing version number. If the 574 | Program specifies that a certain numbered version of the GNU General 575 | Public License "or any later version" applies to it, you have the 576 | option of following the terms and conditions either of that numbered 577 | version or of any later version published by the Free Software 578 | Foundation. If the Program does not specify a version number of the 579 | GNU General Public License, you may choose any version ever published 580 | by the Free Software Foundation. 581 | 582 | If the Program specifies that a proxy can decide which future 583 | versions of the GNU General Public License can be used, that proxy's 584 | public statement of acceptance of a version permanently authorizes you 585 | to choose that version for the Program. 586 | 587 | Later license versions may give you additional or different 588 | permissions. However, no additional obligations are imposed on any 589 | author or copyright holder as a result of your choosing to follow a 590 | later version. 591 | 592 | 15. Disclaimer of Warranty. 593 | 594 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 595 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 596 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 597 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 598 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 599 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 600 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 601 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 602 | 603 | 16. Limitation of Liability. 604 | 605 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 606 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 607 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 608 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 609 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 610 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 611 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 612 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 613 | SUCH DAMAGES. 614 | 615 | 17. Interpretation of Sections 15 and 16. 616 | 617 | If the disclaimer of warranty and limitation of liability provided 618 | above cannot be given local legal effect according to their terms, 619 | reviewing courts shall apply local law that most closely approximates 620 | an absolute waiver of all civil liability in connection with the 621 | Program, unless a warranty or assumption of liability accompanies a 622 | copy of the Program in return for a fee. 623 | 624 | END OF TERMS AND CONDITIONS 625 | 626 | How to Apply These Terms to Your New Programs 627 | 628 | If you develop a new program, and you want it to be of the greatest 629 | possible use to the public, the best way to achieve this is to make it 630 | free software which everyone can redistribute and change under these terms. 631 | 632 | To do so, attach the following notices to the program. It is safest 633 | to attach them to the start of each source file to most effectively 634 | state the exclusion of warranty; and each file should have at least 635 | the "copyright" line and a pointer to where the full notice is found. 636 | 637 | 638 | Copyright (C) 639 | 640 | This program is free software: you can redistribute it and/or modify 641 | it under the terms of the GNU General Public License as published by 642 | the Free Software Foundation, either version 3 of the License, or 643 | (at your option) any later version. 644 | 645 | This program is distributed in the hope that it will be useful, 646 | but WITHOUT ANY WARRANTY; without even the implied warranty of 647 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 648 | GNU General Public License for more details. 649 | 650 | You should have received a copy of the GNU General Public License 651 | along with this program. If not, see . 652 | 653 | Also add information on how to contact you by electronic and paper mail. 654 | 655 | If the program does terminal interaction, make it output a short 656 | notice like this when it starts in an interactive mode: 657 | 658 | Copyright (C) 659 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 660 | This is free software, and you are welcome to redistribute it 661 | under certain conditions; type `show c' for details. 662 | 663 | The hypothetical commands `show w' and `show c' should show the appropriate 664 | parts of the General Public License. Of course, your program's commands 665 | might be different; for a GUI interface, you would use an "about box". 666 | 667 | You should also get your employer (if you work as a programmer) or school, 668 | if any, to sign a "copyright disclaimer" for the program, if necessary. 669 | For more information on this, and how to apply and follow the GNU GPL, see 670 | . 671 | 672 | The GNU General Public License does not permit incorporating your program 673 | into proprietary programs. If your program is a subroutine library, you 674 | may consider it more useful to permit linking proprietary applications with 675 | the library. If this is what you want to do, use the GNU Lesser General 676 | Public License instead of this License. But first, please read 677 | . 678 | ``` -------------------------------------------------------------------------------- /nui/SimpleNotification/notification.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {('top-left' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right')} Position 3 | */ 4 | 5 | /** 6 | * @typedef {('success' | 'info' | 'error' | 'warning' | 'message')} Type 7 | */ 8 | 9 | /** 10 | * @typedef InsertAnimationDefinition 11 | * @type {object} 12 | * @property {('default-insert' 13 | | 'insert-left' 14 | | 'insert-right' 15 | | 'insert-top' 16 | | 'insert-bottom' 17 | | 'fadein' 18 | | 'scalein' 19 | | 'rotatein')} name 20 | * @property {number} duration - in ms 21 | */ 22 | 23 | /** 24 | * @typedef RemoveAnimationDefinition 25 | * @type {object} 26 | * @property {('fadeout' | 'scaleout' | 'rotateout')} name 27 | * @property {number} duration - in ms 28 | */ 29 | 30 | /** 31 | * @typedef EventCallback 32 | * @type {function} 33 | * @param {SimpleNotification} notification 34 | * @returns {void} 35 | */ 36 | 37 | /** 38 | * @typedef OnCloseCallback 39 | * @type {function} 40 | * @param {SimpleNotification} notification 41 | * @param {boolean} [fromUser=false] 42 | * @returns {void} 43 | */ 44 | 45 | /** 46 | * @typedef Events 47 | * @type {object} 48 | * @property {EventCallback} [onCreate]; 49 | * @property {EventCallback} [onDisplay]; 50 | * @property {EventCallback} [onDeath]; 51 | * @property {OnCloseCallback} [onClose]; 52 | */ 53 | 54 | /** 55 | * @typedef Options 56 | * @type {object} 57 | * @property {Position} position 58 | * @property {number} maxNotifications 59 | * @property {boolean} removeAllOnDisplay 60 | * @property {boolean} closeOnClick 61 | * @property {boolean} closeButton 62 | * @property {number} duration 63 | * @property {boolean} sticky 64 | * @property {Events} events 65 | * @property {InsertAnimationDefinition} insertAnimation 66 | * @property {RemoveAnimationDefinition} removeAnimation 67 | */ 68 | 69 | /** 70 | * @typedef Button 71 | * @type {object} 72 | * @property {Type} [type] 73 | * @property {string} [value] 74 | * @property {EventCallback} [onClick] 75 | */ 76 | 77 | /** 78 | * @typedef Content 79 | * @type {object} 80 | * @property {string} [image] 81 | * @property {string} [icon] 82 | * @property {string} [text] 83 | * @property {string} [title] 84 | * @property {Button[]} [buttons] 85 | */ 86 | 87 | /** 88 | * @typedef TagDescription 89 | * @type {object} 90 | * @property {string} type 91 | * @property {string} class 92 | * @property {string} open 93 | * @property {string} close 94 | * @property {{ textContent: string | boolean } & Object.} attributes 95 | * @property {string} textContent 96 | */ 97 | 98 | class SimpleNotification { 99 | /** 100 | * @param {Partial} [options] 101 | */ 102 | constructor(options = undefined) { 103 | /** @type {DocumentFragment} */ 104 | this.fragment = new DocumentFragment(); 105 | /** @type {Options} */ 106 | this.options = options; 107 | if (this.options == undefined) { 108 | this.options = SimpleNotification.deepAssign({}, SimpleNotification._options); 109 | } 110 | /** @type {Events} */ 111 | this.events = this.options.events; 112 | /** @type {HTMLElement | undefined} */ 113 | this.node = undefined; 114 | // Content 115 | /** @type {string | undefined} */ 116 | this.title = undefined; 117 | /** @type {HTMLElement | undefined} */ 118 | this.closeButton = undefined; 119 | /** @type {HTMLElement | undefined} */ 120 | this.body = undefined; 121 | /** @type {HTMLImageElement | undefined} */ 122 | this.image = undefined; 123 | /** @type {HTMLElement | undefined} */ 124 | this.icon = undefined; 125 | /** @type {string | undefined} */ 126 | this.text = undefined; 127 | /** @type {HTMLElement | undefined} */ 128 | this.buttons = undefined; 129 | /** @type {HTMLElement | undefined} */ 130 | this.progressBar = undefined; 131 | // Functions 132 | /** @type {() => void} */ 133 | this.addExtinguish = this.addExtinguishFct.bind(this); 134 | /** @type {() => void} */ 135 | this.removeExtinguish = this.removeExtinguishFct.bind(this); 136 | } 137 | 138 | /** 139 | * @param {object} target 140 | * @param {object[]} objs 141 | * @returns {object} 142 | */ 143 | static deepAssign(target, ...objs) { 144 | for (let i = 0, max = objs.length; i < max; i++) { 145 | for (var k in objs[i]) { 146 | if (objs[i][k] != null && typeof objs[i][k] == 'object') 147 | target[k] = SimpleNotification.deepAssign(target[k] ? target[k] : {}, objs[i][k]); 148 | else target[k] = objs[i][k]; 149 | } 150 | } 151 | return target; 152 | } 153 | 154 | /** 155 | * Set the default options of SimpleNotification 156 | * @param {Partial} options Options object to override the defaults 157 | */ 158 | static options(options) { 159 | SimpleNotification._options = SimpleNotification.deepAssign({}, SimpleNotification._options, options); 160 | } 161 | 162 | /** 163 | * Create a wrapper and add it to the wrappers object 164 | * Valid default position: top-left, top-right, bottom-left, bottom-center, bottom-right 165 | * @param {string} position The position of the wrapper 166 | */ 167 | static makeWrapper(position) { 168 | let wrapper = document.createElement('div'); 169 | wrapper.className = `gn-wrapper gn-${position}`; 170 | document.body.appendChild(wrapper); 171 | SimpleNotification.wrappers[position] = wrapper; 172 | } 173 | 174 | /** 175 | * Search the first occurence of the char occurence in text that doesn't have a \ prefix 176 | * @param {string} text The text where to search the char in 177 | * @param {string} char The string to search in the text 178 | * @param {number} start The position to begin to search with 179 | * @returns {number | undefined} 180 | */ 181 | static firstUnbreakChar(text, char, start = 0) { 182 | if (start < 0) start = 0; 183 | let foundPos; 184 | while (start >= 0) { 185 | foundPos = text.indexOf(char, start); 186 | if (foundPos > 0 && text[foundPos - 1] == '\\') { 187 | start = foundPos + 1; 188 | } else { 189 | start = -1; 190 | } 191 | } 192 | return foundPos; 193 | } 194 | 195 | /** 196 | * Search the first shortest occurence of token in the string array string after position start in the current string 197 | * @param {string} string 198 | * @param {string} token 199 | * @param {number} start 200 | * @returns {[number, number]} 201 | */ 202 | static searchToken(string, token, start) { 203 | let found = [start[0], start[1]]; 204 | for (let max = string.length; found[0] < max; found[0]++) { 205 | if (typeof string[found[0]] == 'string' && (found[1] = string[found[0]].indexOf(token, found[1])) > -1) { 206 | return found; 207 | } 208 | found[1] = 0; 209 | } 210 | return [-1, -1]; 211 | } 212 | 213 | /** 214 | * Break a string with a `tag` element at position start until end 215 | * @param {string} string 216 | * @param {TagDescription} tag 217 | * @param {string} token 218 | * @param {number} start 219 | * @returns {[number, number]} 220 | */ 221 | static breakString(string, tag, start, end) { 222 | let tagLength = { open: tag.open.length, close: tag.close.length }; 223 | if (start[0] != end[0]) { 224 | let inside = { tag: tag, str: [string[start[0]].substring(start[1])] }; 225 | let c = 0; 226 | for (let i = start[0] + 1; i < end[0]; i++, c++) { 227 | inside.str.push(string[i]); 228 | } 229 | inside.str.push(string[end[0]].substring(0, end[1])); 230 | inside.str = [this.joinString(inside.str)]; 231 | string.splice(start[0] + 1, c, inside); 232 | end[0] = start[0] + 2; 233 | string[start[0]] = string[start[0]].substring(0, start[1] - tagLength.open); 234 | string[end[0]] = string[end[0]].substring(end[1] + tagLength.close); 235 | return [end[0], 0]; 236 | } else { 237 | string.splice( 238 | start[0] + 1, 239 | 0, 240 | { tag: tag, str: [string[start[0]].substring(start[1], end[1])] }, 241 | string[start[0]].substring(end[1] + tagLength.close) 242 | ); 243 | string[start[0]] = string[start[0]].substring(0, start[1] - tagLength.open); 244 | return [start[0] + 2, 0]; 245 | } 246 | } 247 | 248 | /** 249 | * Recursive string array concatenation 250 | * @param {string[]} arr 251 | * @returns {string} 252 | */ 253 | static joinString(arr) { 254 | let str = []; 255 | for (let i = 0, max = arr.length; i < max; i++) { 256 | if (typeof arr[i] == 'string') { 257 | str.push(arr[i]); 258 | } else { 259 | str.push(arr[i].tag.open); 260 | str.push(this.joinString(arr[i].str)); 261 | str.push(arr[i].tag.close); 262 | } 263 | } 264 | return str.join(''); 265 | } 266 | 267 | /** 268 | * Make the node body by build each of it's childrens 269 | * @param {string} string 270 | * @param {HTMLElement} node 271 | * @returns {HTMLElement} 272 | */ 273 | static buildNode(string, node) { 274 | for (let i = 0; i < string.length; i++) { 275 | if (typeof string[i] == 'string') { 276 | if (string[i].length > 0) { 277 | node.appendChild(document.createTextNode(string[i])); 278 | } 279 | } else { 280 | let tagInfo = string[i].tag; 281 | let tag = document.createElement(tagInfo.type); 282 | if (tagInfo.type == 'a' || tagInfo.type == 'button') { 283 | tag.addEventListener('click', (event) => { 284 | event.stopPropagation(); 285 | }); 286 | } 287 | // Content 288 | let title; 289 | let content = this.joinString(string[i].str); 290 | if ('title' in tagInfo && tagInfo.title && content.length > 0) { 291 | if (content.indexOf('!') == 0) { 292 | content = content.substring(1); 293 | } else { 294 | // find | 295 | let foundTitleBreak = this.firstUnbreakChar(content, '|'); 296 | content = content.replace('\\|', '|'); 297 | if (foundTitleBreak > -1) { 298 | title = content.substring(0, foundTitleBreak); 299 | content = content.substring(foundTitleBreak + 1); 300 | } 301 | } 302 | } 303 | if (title == undefined) { 304 | title = content; 305 | } 306 | // Set attributes 307 | if ('attributes' in tagInfo) { 308 | let keys = Object.keys(tagInfo.attributes); 309 | for (let k = 0, max = keys.length; k < max; k++) { 310 | let attributeValue = tagInfo.attributes[keys[k]] 311 | .replace('$content', content) 312 | .replace('$title', title); 313 | tag.setAttribute(keys[k], attributeValue); 314 | } 315 | } 316 | if (tagInfo.textContent) { 317 | tag.textContent = tagInfo.textContent.replace('$content', content).replace('$title', title); 318 | } else if (tagInfo.textContent != false) { 319 | this.textToNode(string[i].str, tag); 320 | } 321 | // Set a class if defined 322 | if (tagInfo.class) { 323 | if (Array.isArray(tagInfo.class)) { 324 | for (let i = 0, max = tagInfo.class.length; i < max; i++) { 325 | tag.classList.add(tagInfo.class[i]); 326 | } 327 | } else { 328 | tag.className = tagInfo.class; 329 | } 330 | } 331 | node.appendChild(tag); 332 | } 333 | } 334 | return node; 335 | } 336 | 337 | /** 338 | * Transform a text with tags to a DOM node 339 | * {open}{content}{close} 340 | * {open}{!|title|}{content}{close} | is the title/content separator 341 | * @param {string} text The text with tags 342 | * @param {object} node The node where the text will be added 343 | * @returns {HTMLElement | undefined} 344 | */ 345 | static textToNode(text, node) { 346 | if (text == undefined) return; 347 | let string; 348 | if (Array.isArray(text)) { 349 | string = text; 350 | } else { 351 | // Normalize linebreak 352 | text = text.replace(/(\r?\n|\r)/gm, '\n'); 353 | string = [text]; 354 | } 355 | // Break string by tokens 356 | if (this.tokens == undefined || this.refreshTokens != undefined) { 357 | this.tokens = Object.keys(SimpleNotification.tags); 358 | this.refreshTokens = undefined; 359 | } 360 | for (let i = 0, last = this.tokens.length; i < last; i++) { 361 | let tag = SimpleNotification.tags[this.tokens[i]]; 362 | let tagLength = { open: tag.open.length, close: tag.close.length }; 363 | let continueAt = [0, 0]; 364 | let openPos = [0, 0]; 365 | let closePos = [0, 0]; 366 | while ((openPos = this.searchToken(string, tag.open, continueAt))[0] > -1) { 367 | openPos[1] += tagLength.open; 368 | if ((closePos = this.searchToken(string, tag.close, openPos))[0] > -1) { 369 | continueAt = this.breakString(string, tag, openPos, closePos); 370 | } else { 371 | continueAt = openPos; 372 | } 373 | } 374 | } 375 | return this.buildNode(string, node); 376 | } 377 | 378 | /** 379 | * Create the notification node, set it's classes and call the onCreate event 380 | * @param {string[]} classes 381 | */ 382 | make(classes) { 383 | this.node = document.createElement('div'); 384 | this.fragment.appendChild(this.node); 385 | // Apply Style 386 | this.node.className = 'gn-notification gn-insert'; 387 | if (this.options.insertAnimation.name == 'default-insert') { 388 | switch (this.options.position) { 389 | case 'top-left': 390 | case 'bottom-left': 391 | this.options.insertAnimation.name = 'insert-left'; 392 | break; 393 | case 'top-right': 394 | case 'bottom-right': 395 | this.options.insertAnimation.name = 'insert-right'; 396 | break; 397 | case 'top-center': 398 | this.options.insertAnimation.name = 'insert-top'; 399 | break; 400 | case 'bottom-center': 401 | this.options.insertAnimation.name = 'insert-bottom'; 402 | break; 403 | case 'middle-left': 404 | this.options.insertAnimation.name = 'insert-left'; 405 | break; 406 | case 'middle-right': 407 | this.options.insertAnimation.name = 'insert-right'; 408 | break; 409 | } 410 | } 411 | if (this.options.insertAnimation.name == this.options.removeAnimation.name) { 412 | if (this.options.insertAnimation.name == 'fadeout') { 413 | this.options.removeAnimation.name = 'rotateout'; 414 | } else { 415 | this.options.removeAnimation.name = 'fadeout'; 416 | } 417 | } 418 | this.node.style.animationName = this.options.insertAnimation.name; 419 | this.node.style.animationDuration = `${this.options.insertAnimation.duration}ms`; 420 | classes.forEach((className) => { 421 | this.node.classList.add(className); 422 | }); 423 | // AnimationEnd listener for the different steps of a notification 424 | this.node.addEventListener('animationend', (event) => { 425 | if (event.animationName == this.options.removeAnimation.name) { 426 | this.close(false); 427 | } else if (event.animationName == this.options.insertAnimation.name) { 428 | this.node.classList.remove('gn-insert'); 429 | // Reset notification duration when hovering 430 | // if (!this.options.sticky) { 431 | // this.node.addEventListener('mouseenter', this.removeExtinguish); 432 | // this.node.addEventListener('mouseleave', this.addExtinguish); 433 | // } 434 | if (this.progressBar) { 435 | // Set the time before removing the notification 436 | this.progressBar.style.animationDuration = `${this.options.duration}ms`; 437 | this.progressBar.classList.add('gn-extinguish'); 438 | } 439 | } else if (event.animationName == 'shorten' && this.progressBar) { 440 | // if (!this.options.sticky) { 441 | // this.node.removeEventListener('mouseenter', this.removeExtinguish); 442 | // this.node.removeEventListener('mouseleave', this.addExtinguish); 443 | // } 444 | this.progressBar.classList.add('gn-retire'); 445 | if (this.events.onDeath) { 446 | this.events.onDeath(this); 447 | } else { 448 | this.disableButtons(); 449 | this.closeAnimated(); 450 | // TODO: Add event listener to pause closing 451 | } 452 | } 453 | }); 454 | // Delete the notification on click 455 | if (this.options.closeOnClick) { 456 | this.node.title = 'Click to close.'; 457 | this.node.classList.add('gn-close-on-click'); 458 | this.node.addEventListener('click', () => { 459 | this.close(true); 460 | }); 461 | } 462 | // Fire onCreateEvent 463 | if (this.events.onCreate) { 464 | this.events.onCreate(this); 465 | } 466 | } 467 | 468 | /** 469 | * Set the type of the notification 470 | * success, info, error, warning, message 471 | * It can be another CSS class but `type` will be prepended with `gn-` 472 | * @param {Type} type 473 | */ 474 | setType(type) { 475 | if (this.node) { 476 | let closeOnClick = this.node.classList.contains('gn-close-on-click'); 477 | this.node.className = `gn-notification gn-${type}`; 478 | if (closeOnClick) { 479 | this.node.classList.add('gn-close-on-click'); 480 | } 481 | } 482 | } 483 | 484 | /** 485 | * Set the title of the notification 486 | * @param {string} title 487 | */ 488 | setTitle(title) { 489 | if (this.title == undefined) { 490 | this.title = document.createElement('h1'); 491 | this.node.insertBefore(this.title, this.node.firstElementChild); 492 | if (this.closeButton) { 493 | this.title.appendChild(this.closeButton); 494 | } 495 | } 496 | this.title.title = title; 497 | this.title.textContent = title; 498 | } 499 | 500 | /** 501 | * Add a close button to the top right of the notification 502 | */ 503 | addCloseButton() { 504 | let closeButton = document.createElement('span'); 505 | closeButton.title = 'Click to close.'; 506 | closeButton.className = 'gn-close'; 507 | closeButton.textContent = '\u274C'; 508 | closeButton.addEventListener('click', () => { 509 | this.close(true); 510 | }); 511 | if (this.title) { 512 | closeButton.classList.add('gn-close-title'); 513 | this.title.appendChild(closeButton); 514 | } else { 515 | this.node.insertBefore(closeButton, this.node.firstElementChild); 516 | } 517 | } 518 | 519 | /** 520 | * Add the notification body that contains the notification image and text 521 | */ 522 | addBody() { 523 | this.body = document.createElement('div'); 524 | this.body.className = 'gn-content'; 525 | this.node.appendChild(this.body); 526 | if (this.buttons) { 527 | this.node.insertBefore(this.body, this.buttons); 528 | } else if (this.progressBar) { 529 | this.node.insertBefore(this.body, this.progressBar); 530 | } else { 531 | this.node.appendChild(this.body); 532 | } 533 | } 534 | 535 | /** 536 | * Set the image src attribute 537 | * @param {string} src 538 | */ 539 | setImage(src) { 540 | if (this.image == undefined) { 541 | this.image = document.createElement('img'); 542 | if (this.text) { 543 | this.body.insertBefore(this.image, this.text); 544 | } else { 545 | if (!this.body) { 546 | this.addBody(); 547 | } 548 | this.body.appendChild(this.image); 549 | } 550 | } 551 | this.image.src = src; 552 | } 553 | 554 | /** 555 | * Set the icon attribute 556 | * @param {string} icon 557 | */ 558 | setIcon(icon) { 559 | if (this.ic == undefined) { 560 | this.ic = document.createElement('i'); 561 | if (this.text) { 562 | this.body.insertBefore(this.ic, this.text); 563 | this.ic.classList.add('gn-text-icon'); 564 | } else { 565 | if (!this.body) { 566 | this.addBody(); 567 | } 568 | this.title.insertBefore(this.ic, this.title.firstChild); 569 | this.ic.classList.add('gn-title-icon'); 570 | } 571 | } 572 | 573 | const classes = icon.split(' '); 574 | classes.forEach((className) => { 575 | this.ic.classList.add(className); 576 | }); 577 | } 578 | 579 | /** 580 | * Set the text content of the notification body 581 | * @param {string} content 582 | */ 583 | setText(content) { 584 | if (this.text == undefined) { 585 | this.text = document.createElement('div'); 586 | this.text.className = 'gn-text'; 587 | if (!this.body) { 588 | this.addBody(); 589 | } 590 | this.body.appendChild(this.text); 591 | } else { 592 | while (this.text.firstChild) { 593 | this.text.removeChild(this.text.firstChild); 594 | } 595 | } 596 | SimpleNotification.textToNode(content, this.text); 597 | } 598 | 599 | /** 600 | * Add a single button after all already added buttons 601 | * @param {Button} options 602 | */ 603 | addButton(options) { 604 | if (!options.type || !options.value) return; 605 | if (this.buttons == undefined) { 606 | this.buttons = document.createElement('div'); 607 | this.buttons.className = 'gn-buttons'; 608 | if (this.progressBar) { 609 | this.node.insertBefore(this.buttons, this.progressBar); 610 | } else { 611 | this.node.appendChild(this.buttons); 612 | } 613 | } 614 | let button = document.createElement('button'); 615 | SimpleNotification.textToNode(options.value, button); 616 | button.className = `gn-button gn-${options.type}`; 617 | if (options.onClick) { 618 | button.addEventListener('click', (event) => { 619 | event.stopPropagation(); 620 | options.onClick(this); 621 | }); 622 | } 623 | this.buttons.appendChild(button); 624 | } 625 | 626 | /** 627 | * Remove all buttons 628 | */ 629 | removeButtons() { 630 | if (this.buttons) { 631 | this.node.removeChild(this.buttons); 632 | this.buttons = undefined; 633 | } 634 | } 635 | 636 | /** 637 | * Add the notification progress bar 638 | */ 639 | addProgressBar() { 640 | this.progressBar = document.createElement('span'); 641 | this.progressBar.className = 'gn-lifespan'; 642 | this.node.appendChild(this.progressBar); 643 | } 644 | 645 | /** 646 | * Append the notification body to it's wrapper and call the onDisplay event 647 | */ 648 | display() { 649 | if (this.node) { 650 | if (this.options.removeAllOnDisplay) { 651 | SimpleNotification.displayed.forEach((n) => { 652 | n.remove(); 653 | }); 654 | } else if (this.options.maxNotifications > 0) { 655 | let diff = -(this.options.maxNotifications - (SimpleNotification.displayed.length + 1)); 656 | if (diff > 0) { 657 | for (let i = 0, max = diff; i < max; i++) { 658 | SimpleNotification.displayed[i].remove(); 659 | } 660 | } 661 | } 662 | if (!SimpleNotification.wrappers[this.options.position]) { 663 | SimpleNotification.makeWrapper(this.options.position); 664 | } 665 | SimpleNotification.wrappers[this.options.position].appendChild(this.fragment); 666 | SimpleNotification.displayed.push(this); 667 | if (this.events.onDisplay) { 668 | this.events.onDisplay(this); 669 | } 670 | } 671 | } 672 | 673 | /** 674 | * Remove the notification from the screen without calling the onClose event 675 | * @returns {boolean} 676 | */ 677 | remove() { 678 | if (this.node != undefined) { 679 | this.node.remove(); 680 | this.node = undefined; 681 | let index = SimpleNotification.displayed.indexOf(this); 682 | if (index) { 683 | SimpleNotification.displayed.splice(index, 1); 684 | } 685 | return true; 686 | } 687 | return false; 688 | } 689 | 690 | /** 691 | * Remove the notification from the screen and call the onClose event 692 | * @param {boolean} fromUser 693 | */ 694 | close(fromUser = false) { 695 | if (this.remove() && this.events.onClose) { 696 | this.events.onClose(this, fromUser); 697 | } 698 | } 699 | 700 | /** 701 | * Remove reset events and add the fadeout animation 702 | */ 703 | closeAnimated() { 704 | // Add the fadeout animation 705 | this.node.classList.add('gn-remove'); 706 | this.node.style.animationName = this.options.removeAnimation.name; 707 | this.node.style.animationDuration = `${this.options.removeAnimation.duration}ms`; 708 | // Pause and reset fadeout on hover 709 | // this.node.addEventListener('mouseenter', (event) => { 710 | // event.target.classList.remove('gn-remove'); 711 | // }); 712 | // this.node.addEventListener('mouseleave', (event) => { 713 | // event.target.classList.add('gn-remove'); 714 | // }); 715 | } 716 | 717 | /** 718 | * Add the class 'gn-extinguish' to the event target 719 | * Used in create() and closeAnimated() to be able to remove the eventListener. 720 | */ 721 | addExtinguishFct() { 722 | this.progressBar.classList.add('gn-extinguish'); 723 | } 724 | 725 | /** 726 | * Remove the class 'gn-extinguish' to the event target 727 | * Used in create() and closeAnimated() to be able to remove the eventListener. 728 | */ 729 | removeExtinguishFct() { 730 | this.progressBar.classList.remove('gn-extinguish'); 731 | } 732 | 733 | /** 734 | * Add the disabled state to all displayed buttons 735 | */ 736 | disableButtons() { 737 | if (this.buttons) { 738 | for (let i = 0, max = this.buttons.childNodes.length; i < max; i++) { 739 | this.buttons.childNodes[i].disabled = true; 740 | } 741 | } 742 | } 743 | 744 | /** 745 | * Create and append a notification 746 | * content is an object with the keys title, text, image and buttons 747 | * Options: duration, fadeout, position 748 | * @param {array} classes Array of classes to add to the notification 749 | * @param {Content} content The content the notification 750 | * @param {Partial} options The options of the notifications 751 | * @returns {SimpleNotification} 752 | */ 753 | static create(classes, content, notificationOptions = {}) { 754 | let hasImage = 'image' in content && content.image, 755 | hasIcon = 'icon' in content && content.icon, 756 | hasText = 'text' in content && content.text, 757 | hasTitle = 'title' in content && content.title, 758 | hasButtons = 'buttons' in content; 759 | // Abort if empty 760 | if (!hasImage && !hasTitle && !hasText && !hasButtons) return; 761 | // Merge options 762 | let options = SimpleNotification.deepAssign({}, SimpleNotification._options, notificationOptions); 763 | // Create the notification 764 | let notification = new SimpleNotification(options); 765 | notification.make(classes); 766 | // Add elements 767 | if (hasTitle) { 768 | notification.setTitle(content.title); 769 | } 770 | if (options.closeButton) { 771 | notification.addCloseButton(); 772 | } 773 | if (hasImage) { 774 | notification.setImage(content.image); 775 | } 776 | if (hasText) { 777 | notification.setText(content.text); 778 | } 779 | if (hasIcon) { 780 | notification.setIcon(content.icon); 781 | } 782 | if (hasButtons) { 783 | if (!Array.isArray(content.buttons)) { 784 | content.buttons = [content.buttons]; 785 | } 786 | for (let i = 0, max = content.buttons.length; i < max; i++) { 787 | notification.addButton(content.buttons[i]); 788 | } 789 | } 790 | // Add progress bar if not sticky 791 | if (!options.sticky) { 792 | notification.addProgressBar(); 793 | } 794 | // Display 795 | if (!('display' in options) || options.display) { 796 | notification.display(); 797 | } 798 | return notification; 799 | } 800 | 801 | /** 802 | * Create a notification with the 'success' style 803 | * @param {Content} content Content of the notification 804 | * @param {Partial} options Options used for the notification 805 | * @returns {SimpleNotification} 806 | */ 807 | static success(content, options = {}) { 808 | return this.create(['gn-success'], content, options); 809 | } 810 | 811 | /** 812 | * Create a notification with the 'info' style 813 | * @param {Content} content Content of the notification 814 | * @param {Partial} options Options used for the notification 815 | * @returns {SimpleNotification} 816 | */ 817 | static info(content, options = {}) { 818 | return this.create(['gn-info'], content, options); 819 | } 820 | 821 | /** 822 | * Create a notification with the 'error' style 823 | * @param {Content} content Content of the notification 824 | * @param {Partial} options Options used for the notification 825 | * @returns {SimpleNotification} 826 | */ 827 | static error(content, options = {}) { 828 | return this.create(['gn-error'], content, options); 829 | } 830 | 831 | /** 832 | * Create a notification with the 'warning' style 833 | * @param {Content} content Content of the notification 834 | * @param {Partial} options Options used for the notification 835 | * @returns {SimpleNotification} 836 | */ 837 | static warning(content, options = {}) { 838 | return this.create(['gn-warning'], content, options); 839 | } 840 | 841 | /** 842 | * Create a notification with the 'message' style 843 | * @param {Content} content Content of the notification 844 | * @param {Partial} options Options used for the notification 845 | * @returns {SimpleNotification} 846 | */ 847 | static message(content, options = {}) { 848 | return this.create(['gn-message'], content, options); 849 | } 850 | 851 | /** 852 | * Make a notification with custom classes 853 | * @param {string[]} classes The classes of the notification 854 | * @param {Content} content Content of the notification 855 | * @param {Partial} options Options used for the notification 856 | * @returns {SimpleNotification} 857 | */ 858 | static custom(classes, content, options = {}) { 859 | return this.create(classes, content, options); 860 | } 861 | 862 | /** 863 | * Add a tag for the textToNode function 864 | * @param {string} name The name of the tag 865 | * @param {TagDescription} object The values of the tag 866 | */ 867 | static addTag(name, object) { 868 | this.tags[name] = object; 869 | this.refreshTokens = true; 870 | } 871 | } 872 | /** 873 | * @type {Object.} 874 | */ 875 | SimpleNotification.wrappers = {}; 876 | /** 877 | * @type {SimpleNotification[]} 878 | */ 879 | SimpleNotification.displayed = []; 880 | /** 881 | * @type {Options} 882 | */ 883 | SimpleNotification._options = { 884 | position: 'top-right', 885 | maxNotifications: 0, 886 | removeAllOnDisplay: false, 887 | closeOnClick: true, 888 | closeButton: true, 889 | duration: 4000, 890 | sticky: false, 891 | events: { 892 | onCreate: undefined, 893 | onDisplay: undefined, 894 | onDeath: undefined, 895 | onClose: undefined, 896 | }, 897 | insertAnimation: { 898 | name: 'default-insert', 899 | duration: 250, 900 | }, 901 | removeAnimation: { 902 | name: 'fadeout', 903 | duration: 400, 904 | }, 905 | }; 906 | /** 907 | * @type {Object.} 908 | */ 909 | SimpleNotification.tags = { 910 | code: { 911 | type: 'code', 912 | class: 'gn-code', 913 | open: '``', 914 | close: '``', 915 | textContent: '$content', 916 | }, 917 | floatRight: { 918 | type: 'span', 919 | class: 'gn-float-right', 920 | open: '>>', 921 | close: '<', 922 | }, 923 | header2: { 924 | type: 'h2', 925 | class: 'gn-header', 926 | open: '## ', 927 | close: '\n', 928 | }, 929 | header1: { 930 | type: 'h1', 931 | class: 'gn-header', 932 | open: '# ', 933 | close: '\n', 934 | }, 935 | image: { 936 | type: 'img', 937 | title: true, 938 | attributes: { 939 | src: '$content', 940 | title: '$title', 941 | }, 942 | textContent: false, 943 | open: '![', 944 | close: ']', 945 | }, 946 | link: { 947 | type: 'a', 948 | title: true, 949 | attributes: { 950 | href: '$content', 951 | target: 'blank', 952 | title: '$title', 953 | }, 954 | textContent: '$title', 955 | open: '{{', 956 | close: '}}', 957 | }, 958 | bold: { 959 | type: 'span', 960 | class: 'gn-bold', 961 | open: '**', 962 | close: '**', 963 | }, 964 | italic: { 965 | type: 'span', 966 | class: 'gn-italic', 967 | open: '*', 968 | close: '*', 969 | }, 970 | separator: { 971 | type: 'div', 972 | class: 'gn-separator', 973 | textContent: false, 974 | open: '\n---\n', 975 | close: '', 976 | }, 977 | linejump: { 978 | type: 'br', 979 | textContent: false, 980 | open: '\n', 981 | close: '', 982 | }, 983 | red: { 984 | type: 'span', 985 | class: 'gn-red', 986 | open: '~r~', 987 | close: '~r~' 988 | }, 989 | green: { 990 | type: 'span', 991 | class: 'gn-green', 992 | open: '~g~', 993 | close: '~g~' 994 | }, 995 | yellow: { 996 | type: 'span', 997 | class: 'gn-yellow', 998 | open: '~y~', 999 | close: '~y~' 1000 | }, 1001 | blue: { 1002 | type: 'span', 1003 | class: 'gn-blue', 1004 | open: '~b~', 1005 | close: '~b~' 1006 | }, 1007 | cyan: { 1008 | type: 'span', 1009 | class: 'gn-cyan', 1010 | open: '~c~', 1011 | close: '~c~' 1012 | }, 1013 | purple: { 1014 | type: 'span', 1015 | class: 'gn-purple', 1016 | open: '~p~', 1017 | close: '~p~' 1018 | }, 1019 | white: { 1020 | type: 'span', 1021 | class: 'gn-white', 1022 | open: '~w~', 1023 | close: '~w~' 1024 | }, 1025 | orange: { 1026 | type: 'span', 1027 | class: 'gn-orange', 1028 | open: '~o~', 1029 | close: '~o~' 1030 | }, 1031 | gray: { 1032 | type: 'span', 1033 | class: 'gn-gray', 1034 | open: '~gy~', 1035 | close: '~gy~' 1036 | } 1037 | }; 1038 | --------------------------------------------------------------------------------