35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://discord.gg/ewvbgb5)
2 | [](https://opensource.org/licenses/gpl-3.0.html)
3 | [](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 | 
32 | 
33 | 
34 | 
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 | 
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 | 
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 | 
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 | 
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 | 
326 |
327 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------