Last active 1719126347

surreal.js Raw
1// Welcome to Surreal 1.2.1 (slightly modified)
2// Documentation: https://github.com/gnat/surreal
3// Locality of Behavior (LoB): https://htmx.org/essays/locality-of-behaviour/
4let surreal = (function () {
5let $ = { // Convenience for internals.
6 $: this, // Convenience for internals.
7 plugins: [],
8
9 // Table of contents and convenient call chaining sugar. For a familiar "jQuery like" syntax. 🙂
10 // Check before adding new: https://youmightnotneedjquery.com/
11 sugar(e) {
12 if (e == null) { console.warn(`Surreal: Cannot use "${e}". Missing a character?`) }
13 if (e.hasOwnProperty('hasSurreal')) return e // Surreal already applied
14
15 // General
16 e.run = (value) => { return $.run(e, value) }
17 e.remove = () => { return $.remove(e) }
18
19 // Classes and CSS.
20 e.addClass = (name) => { return $.addClass(e, name) }
21 e.removeClass = (name) => { return $.removeClass(e, name) }
22 e.toggleClass = (name) => { return $.toggleClass(e, name) }
23 e.styles = (value) => { return $.styles(e, value) }
24
25 // Events.
26 e.on = (name, fn) => { return $.on(e, name, fn) }
27 e.off = (name, fn) => { return $.off(e, name, fn) }
28 e.offAll = (name) => { return $.offAll(e, name) }
29 e.disable = () => { return $.disable(e) }
30 e.enable = () => { return $.enable(e) }
31 e.send = (name, detail) => { return $.send(e, name, detail) }
32 e.trigger = e.send // Alias
33 e.halt = (ev, keepBubbling, keepDefault) => { return $.halt(ev, keepBubbling, keepDefault) }
34
35 // Attributes.
36 e.attribute = (name, value) => { return $.attribute(e, name, value) }
37 e.attributes = e.attr = e.attribute // Alias
38
39 // Add all plugins.
40 $.plugins.forEach(function(func) { func(e) })
41
42 e.hasSurreal = 1
43 return e
44 },
45 // Return single element. Selector not needed if used with inline <script> 🔥
46 // If your query returns a collection, it will return the first element.
47 // Example
48 // <div>
49 // Hello World!
50 // <script>me().style.color = 'red'</script>
51 // </div>
52 me(selector=null, start=document, warning=true) {
53 if (selector == null) return $.sugar(start.currentScript.parentElement) // Just local me() in <script>
54 if (selector instanceof Event) return selector.currentTarget ? $.me(selector.currentTarget) : (console.warn(`Surreal: Event currentTarget is null. Please save your element because async will lose it`), null) // Events try currentTarget
55 if (selector === '-' || selector === 'prev' || selector === 'previous') return $.sugar(start.currentScript.previousElementSibling) // Element directly before <script>
56 if ($.isSelector(selector, start, warning)) return $.sugar(start.querySelector(selector)) // String selector.
57 if ($.isNodeList(selector)) return $.me(selector[0]) // If we got a list, just take the first element.
58 if ($.isNode(selector)) return $.sugar(selector) // Valid element.
59 return null // Invalid.
60 },
61 // any() is me() but always returns array of elements. Requires selector.
62 // Returns an Array of elements (so you can use methods like forEach/filter/map/reduce if you want).
63 // Example: any('button')
64 any(selector, start=document, warning=true) {
65 if (selector == null) return $.sugar([start.currentScript.parentElement]) // Just local me() in <script>
66 if (selector instanceof Event) return selector.currentTarget ? $.any(selector.currentTarget) : (console.warn(`Surreal: Event currentTarget is null. Please save your element because async will lose it`), null) // Events try currentTarget
67 if (selector === '-' || selector === 'prev' || selector === 'previous') return $.sugar([start.currentScript.previousElementSibling]) // Element directly before <script>
68 if ($.isSelector(selector, start, warning)) return $.sugar(Array.from(start.querySelectorAll(selector))) // String selector.
69 if ($.isNode(selector)) return $.sugar([selector]) // Single element. Convert to Array.
70 if ($.isNodeList(selector)) return $.sugar(Array.from(selector)) // Valid NodeList or Array.
71 return null // Invalid.
72 },
73 // Run any function on element(s)
74 run(e, f) {
75 if ($.isNodeList(e)) e.forEach(_ => { $.run(_, f) })
76 if ($.isNode(e)) { f(e); }
77 return e
78 },
79 // Remove element(s)
80 remove(e) {
81 if ($.isNodeList(e)) e.forEach(_ => { $.remove(_) })
82 if ($.isNode(e)) e.parentNode.removeChild(e)
83 return // Special, end of chain.
84 },
85 // Add class to element(s).
86 addClass(e, name) {
87 if (e === null || (Array.isArray(e) && e.length === 0)) return null
88 if (typeof name !== 'string') return null
89 if (name.charAt(0) === '.') name = name.substring(1)
90 if ($.isNodeList(e)) e.forEach(_ => { $.addClass(_, name) })
91 if ($.isNode(e)) e.classList.add(name)
92 return e
93 },
94 // Remove class from element(s).
95 removeClass(e, name) {
96 if (typeof name !== 'string') return null
97 if (name.charAt(0) === '.') name = name.substring(1)
98 if ($.isNodeList(e)) e.forEach(_ => { $.removeClass(_, name) })
99 if ($.isNode(e)) e.classList.remove(name)
100 return e
101 },
102 // Toggle class in element(s).
103 toggleClass(e, name) {
104 if (typeof name !== 'string') return null
105 if (name.charAt(0) === '.') name = name.substring(1)
106 if ($.isNodeList(e)) e.forEach(_ => { $.toggleClass(_, name) })
107 if ($.isNode(e)) e.classList.toggle(name)
108 return e
109 },
110 // Add inline style to element(s).
111 // Can use string or object formats.
112 // String format: "font-family: 'sans-serif'"
113 // Object format; { fontFamily: 'sans-serif', backgroundColor: '#000' }
114 styles(e, value) {
115 if (typeof value === 'string') { // Format: "font-family: 'sans-serif'"
116 if ($.isNodeList(e)) e.forEach(_ => { $.styles(_, value) })
117 if ($.isNode(e)) { $.attribute(e, 'style', ($.attribute(e, 'style') == null ? '' : $.attribute(e, 'style') + '; ') + value) }
118 return e
119 }
120 if (typeof value === 'object') { // Format: { fontFamily: 'sans-serif', backgroundColor: '#000' }
121 if ($.isNodeList(e)) e.forEach(_ => { $.styles(_, value) })
122 if ($.isNode(e)) { Object.assign(e.style, value) }
123 return e
124 }
125 },
126 // Add event listener to element(s).
127 // Match a sender: if(!event.target.matches(".selector")) return;
128 // 📚️ https://developer.mozilla.org/en-US/docs/Web/API/Event
129 // ✂️ Vanilla: document.querySelector(".thing").addEventListener("click", (e) => { alert("clicked") }
130 on(e, name, fn) {
131 if (typeof name !== 'string') return null
132 if ($.isNodeList(e)) e.forEach(_ => { $.on(_, name, fn) })
133 if ($.isNode(e)) e.addEventListener(name, fn)
134 return e
135 },
136 off(e, name, fn) {
137 if (typeof name !== 'string') return null
138 if ($.isNodeList(e)) e.forEach(_ => { $.off(_, name, fn) })
139 if ($.isNode(e)) e.removeEventListener(name, fn)
140 return e
141 },
142 offAll(e) {
143 if ($.isNodeList(e)) e.forEach(_ => { $.offAll(_) })
144 if ($.isNode(e)) e.parentNode.replaceChild(e.cloneNode(true), e)
145 return e
146 },
147 // Easy alternative to off(). Disables click, key, submit events.
148 disable(e) {
149 if ($.isNodeList(e)) e.forEach(_ => { $.disable(_) })
150 if ($.isNode(e)) e.disabled = true
151 return e
152 },
153 // For reversing disable()
154 enable(e) {
155 if ($.isNodeList(e)) e.forEach(_ => { $.enable(_) })
156 if ($.isNode(e)) e.disabled = false
157 return e
158 },
159 // Send / trigger event.
160 // ✂️ Vanilla: Events Dispatch: document.querySelector(".thing").dispatchEvent(new Event('click'))
161 send(e, name, detail=null) {
162 if ($.isNodeList(e)) e.forEach(_ => { $.send(_, name, detail) })
163 if ($.isNode(e)) {
164 const event = new CustomEvent(name, { detail: detail, bubbles: true })
165 e.dispatchEvent(event)
166 }
167 return e
168 },
169 // Halt event. Default: Stops normal event actions and event propagation.
170 halt(ev, keepBubbling=false, keepDefault=false) {
171 if (ev instanceof Event) {
172 if(!keepDefault) ev.preventDefault()
173 if(!keepBubbling) ev.stopPropagation()
174 }
175 return ev
176 },
177 // Add or remove attributes from element(s)
178 attribute(e, name, value=undefined) {
179 // Get. (Format: "name", "value") Special: Ends call chain.
180 if (typeof name === 'string' && value === undefined) {
181 if ($.isNodeList(e)) return [] // Not supported for Get. For many elements, wrap attribute() in any(...).run(...) or any(...).forEach(...)
182 if ($.isNode(e)) return e.getAttribute(name)
183 return null // No value. Ends call chain.
184 }
185 // Remove.
186 if (typeof name === 'string' && value === null) {
187 if ($.isNodeList(e)) e.forEach(_ => { $.attribute(_, name, value) })
188 if ($.isNode(e)) e.removeAttribute(name)
189 return e
190 }
191 // Add / Set.
192 if (typeof name === 'string') {
193 if ($.isNodeList(e)) e.forEach(_ => { $.attribute(_, name, value) })
194 if ($.isNode(e)) e.setAttribute(name, value)
195 return e
196 }
197 // Format: { "name": "value", "blah": true }
198 if (typeof name === 'object') {
199 if ($.isNodeList(e)) e.forEach(_ => { Object.entries(name).forEach(([key, val]) => { $.attribute(_, key, val) }) })
200 if ($.isNode(e)) Object.entries(name).forEach(([key, val]) => { $.attribute(e, key, val) })
201 return e
202 }
203 return e
204 },
205 // Puts Surreal functions except for "restricted" in global scope.
206 globalsAdd() {
207 console.log(`Surreal: Adding convenience globals to window.`)
208 let restricted = ['$', 'sugar']
209 for (const [key, value] of Object.entries(this)) {
210 if (!restricted.includes(key)) window[key] != 'undefined' ? window[key] = value : console.warn(`Surreal: "${key}()" already exists on window. Skipping to prevent overwrite.`)
211 window.document[key] = value
212 }
213 },
214 // ⚙️ Used internally. Is this an element / node?
215 isNode(e) {
216 return (e instanceof HTMLElement || e instanceof SVGElement) ? true : false
217 },
218 // ⚙️ Used internally by DOM functions. Is this a list of elements / nodes?
219 isNodeList(e) {
220 return (e instanceof NodeList || Array.isArray(e)) ? true : false
221 },
222 // ⚙️ Used internally by DOM functions. Warning when selector is invalid. Likely missing a "#" or "."
223 isSelector(selector="", start=document, warning=true) {
224 if(typeof selector !== 'string') return false
225 if (start.querySelector(selector) == null) {
226 if (warning) console.warn(`Surreal: "${selector}" was not found. Missing a character? (. #)`)
227 return false
228 }
229 return true // Valid.
230 },
231}
232// Finish up...
233$.globalsAdd() // Full convenience.
234console.log("Surreal: Loaded.")
235return $
236})() // End of Surreal 👏
237
238// 🔌 Plugin: Effects
239function pluginEffects(e) {
240 // Fade out and remove element.
241 // Equivalent to jQuery fadeOut(), but actually removes the element!
242 function fadeOut(e, fn=false, ms=1000, remove=true) {
243 let thing = e
244 if (surreal.isNodeList(e)) e.forEach(_ => { fadeOut(_, fn, ms) })
245 if (surreal.isNode(e)) {
246 (async() => {
247 surreal.styles(e, {transform: 'scale(1)', transition: `all ${ms}ms ease-out`, overflow: 'hidden'})
248 await tick()
249 surreal.styles(e, {transform: 'scale(0.9)', opacity: '0'})
250 await sleep(ms, e)
251 if (typeof fn === 'function') fn(thing) // Run custom callback?
252 if (remove) surreal.remove(thing) // Remove element after animation is completed?
253 })()
254 }
255 }
256 // Fade in an element that has opacity under 1
257 function fadeIn(e, fn=false, ms=1000) {
258 let thing = e
259 if(surreal.isNodeList(e)) e.forEach(_ => { fadeIn(_, fn, ms) })
260 if(surreal.isNode(e)) {
261 (async() => {
262 let save = e.style // Store original style.
263 surreal.styles(e, {transition: `all ${ms}ms ease-in`, overflow: 'hidden'})
264 await tick()
265 surreal.styles(e, {opacity: '1'})
266 await sleep(ms, e)
267 e.style = save // Revert back to original style.
268 surreal.styles(e, {opacity: '1'}) // Ensure we're visible after reverting to original style.
269 if (typeof fn === 'function') fn(thing) // Run custom callback?
270 })()
271 }
272 }
273 // Add sugar
274 e.fadeOut = (fn, ms, remove) => { return fadeOut(e, fn, ms, remove) }
275 e.fade_out = e.fadeOut
276 e.fadeIn = (fn, ms) => { return fadeIn(e, fn, ms) }
277 e.fade_in = e.fadeIn
278}
279
280// 🔌 Add plugins here!
281surreal.plugins.push(pluginEffects)
282console.log("Surreal: Added plugins.")
283
284// 🌐 Add global shortcuts here!
285// DOM.
286const createElement = create_element = document.createElement.bind(document)
287// Animation.
288const rAF = typeof requestAnimationFrame !== 'undefined' && requestAnimationFrame
289const rIC = typeof requestIdleCallback !== 'undefined' && requestIdleCallback
290// Animation: Wait for next animation frame, non-blocking.
291async function tick() {
292 return await new Promise(resolve => { requestAnimationFrame(resolve) })
293}
294// Animation: Sleep, non-blocking.
295async function sleep(ms, e) {
296 return await new Promise(resolve => setTimeout(() => { resolve(e) }, ms))
297}
298// Loading: Why? So you don't clobber window.onload (predictable sequential loading)
299// Example: <script>onloadAdd(() => { console.log("Page was loaded!") })</script>
300// Example: <script>onloadAdd(() => { console.log("Lets do another thing without clobbering window.onload!") })</script>
301const onloadAdd = addOnload = onload_add = add_onload = (f) => {
302 if (typeof window.onload === 'function') { // window.onload already is set, queue functions together (creates a call chain).
303 let onload_old = window.onload
304 window.onload = () => {
305 onload_old()
306 f()
307 }
308 return
309 }
310 window.onload = f // window.onload was not set yet.
311}
312console.log("Surreal: Added shortcuts.")
313

Powered by Opengist Load: 9ms