Last active 1719126347

prologic revised this gist 1719126347. Go to revision

1 file changed, 312 insertions

surreal.js(file created)

@@ -0,0 +1,312 @@
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/
4 + let surreal = (function () {
5 + let $ = { // 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.
234 + console.log("Surreal: Loaded.")
235 + return $
236 + })() // End of Surreal 👏
237 +
238 + // 🔌 Plugin: Effects
239 + function 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!
281 + surreal.plugins.push(pluginEffects)
282 + console.log("Surreal: Added plugins.")
283 +
284 + // 🌐 Add global shortcuts here!
285 + // DOM.
286 + const createElement = create_element = document.createElement.bind(document)
287 + // Animation.
288 + const rAF = typeof requestAnimationFrame !== 'undefined' && requestAnimationFrame
289 + const rIC = typeof requestIdleCallback !== 'undefined' && requestIdleCallback
290 + // Animation: Wait for next animation frame, non-blocking.
291 + async function tick() {
292 + return await new Promise(resolve => { requestAnimationFrame(resolve) })
293 + }
294 + // Animation: Sleep, non-blocking.
295 + async 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>
301 + const 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 + }
312 + console.log("Surreal: Added shortcuts.")
Newer Older

Powered by Opengist Load: 20ms