surreal.js
Raw
// Welcome to Surreal 1.2.1 (slightly modified)
// Documentation: https://github.com/gnat/surreal
// Locality of Behavior (LoB): https://htmx.org/essays/locality-of-behaviour/
let surreal = (function () {
let $ = { // Convenience for internals.
$: this, // Convenience for internals.
plugins: [],
// Table of contents and convenient call chaining sugar. For a familiar "jQuery like" syntax. 🙂
// Check before adding new: https://youmightnotneedjquery.com/
sugar(e) {
if (e == null) { console.warn(`Surreal: Cannot use "${e}". Missing a character?`) }
if (e.hasOwnProperty('hasSurreal')) return e // Surreal already applied
// General
e.run = (value) => { return $.run(e, value) }
e.remove = () => { return $.remove(e) }
// Classes and CSS.
e.addClass = (name) => { return $.addClass(e, name) }
e.removeClass = (name) => { return $.removeClass(e, name) }
e.toggleClass = (name) => { return $.toggleClass(e, name) }
e.styles = (value) => { return $.styles(e, value) }
// Events.
e.on = (name, fn) => { return $.on(e, name, fn) }
e.off = (name, fn) => { return $.off(e, name, fn) }
e.offAll = (name) => { return $.offAll(e, name) }
e.disable = () => { return $.disable(e) }
e.enable = () => { return $.enable(e) }
e.send = (name, detail) => { return $.send(e, name, detail) }
e.trigger = e.send // Alias
e.halt = (ev, keepBubbling, keepDefault) => { return $.halt(ev, keepBubbling, keepDefault) }
// Attributes.
e.attribute = (name, value) => { return $.attribute(e, name, value) }
e.attributes = e.attr = e.attribute // Alias
// Add all plugins.
$.plugins.forEach(function(func) { func(e) })
e.hasSurreal = 1
return e
},
// Return single element. Selector not needed if used with inline <script> 🔥
// If your query returns a collection, it will return the first element.
// Example
// <div>
// Hello World!
// <script>me().style.color = 'red'</script>
// </div>
me(selector=null, start=document, warning=true) {
if (selector == null) return $.sugar(start.currentScript.parentElement) // Just local me() in <script>
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
if (selector === '-' || selector === 'prev' || selector === 'previous') return $.sugar(start.currentScript.previousElementSibling) // Element directly before <script>
if ($.isSelector(selector, start, warning)) return $.sugar(start.querySelector(selector)) // String selector.
if ($.isNodeList(selector)) return $.me(selector[0]) // If we got a list, just take the first element.
if ($.isNode(selector)) return $.sugar(selector) // Valid element.
return null // Invalid.
},
// any() is me() but always returns array of elements. Requires selector.
// Returns an Array of elements (so you can use methods like forEach/filter/map/reduce if you want).
// Example: any('button')
any(selector, start=document, warning=true) {
if (selector == null) return $.sugar([start.currentScript.parentElement]) // Just local me() in <script>
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
if (selector === '-' || selector === 'prev' || selector === 'previous') return $.sugar([start.currentScript.previousElementSibling]) // Element directly before <script>
if ($.isSelector(selector, start, warning)) return $.sugar(Array.from(start.querySelectorAll(selector))) // String selector.
if ($.isNode(selector)) return $.sugar([selector]) // Single element. Convert to Array.
if ($.isNodeList(selector)) return $.sugar(Array.from(selector)) // Valid NodeList or Array.
return null // Invalid.
},
// Run any function on element(s)
run(e, f) {
if ($.isNodeList(e)) e.forEach(_ => { $.run(_, f) })
if ($.isNode(e)) { f(e); }
return e
},
// Remove element(s)
remove(e) {
if ($.isNodeList(e)) e.forEach(_ => { $.remove(_) })
if ($.isNode(e)) e.parentNode.removeChild(e)
return // Special, end of chain.
},
// Add class to element(s).
addClass(e, name) {
if (e === null || (Array.isArray(e) && e.length === 0)) return null
if (typeof name !== 'string') return null
if (name.charAt(0) === '.') name = name.substring(1)
if ($.isNodeList(e)) e.forEach(_ => { $.addClass(_, name) })
if ($.isNode(e)) e.classList.add(name)
return e
},
// Remove class from element(s).
removeClass(e, name) {
if (typeof name !== 'string') return null
if (name.charAt(0) === '.') name = name.substring(1)
if ($.isNodeList(e)) e.forEach(_ => { $.removeClass(_, name) })
if ($.isNode(e)) e.classList.remove(name)
return e
},
// Toggle class in element(s).
toggleClass(e, name) {
if (typeof name !== 'string') return null
if (name.charAt(0) === '.') name = name.substring(1)
if ($.isNodeList(e)) e.forEach(_ => { $.toggleClass(_, name) })
if ($.isNode(e)) e.classList.toggle(name)
return e
},
// Add inline style to element(s).
// Can use string or object formats.
// String format: "font-family: 'sans-serif'"
// Object format; { fontFamily: 'sans-serif', backgroundColor: '#000' }
styles(e, value) {
if (typeof value === 'string') { // Format: "font-family: 'sans-serif'"
if ($.isNodeList(e)) e.forEach(_ => { $.styles(_, value) })
if ($.isNode(e)) { $.attribute(e, 'style', ($.attribute(e, 'style') == null ? '' : $.attribute(e, 'style') + '; ') + value) }
return e
}
if (typeof value === 'object') { // Format: { fontFamily: 'sans-serif', backgroundColor: '#000' }
if ($.isNodeList(e)) e.forEach(_ => { $.styles(_, value) })
if ($.isNode(e)) { Object.assign(e.style, value) }
return e
}
},
// Add event listener to element(s).
// Match a sender: if(!event.target.matches(".selector")) return;
// 📚️ https://developer.mozilla.org/en-US/docs/Web/API/Event
// ✂️ Vanilla: document.querySelector(".thing").addEventListener("click", (e) => { alert("clicked") }
on(e, name, fn) {
if (typeof name !== 'string') return null
if ($.isNodeList(e)) e.forEach(_ => { $.on(_, name, fn) })
if ($.isNode(e)) e.addEventListener(name, fn)
return e
},
off(e, name, fn) {
if (typeof name !== 'string') return null
if ($.isNodeList(e)) e.forEach(_ => { $.off(_, name, fn) })
if ($.isNode(e)) e.removeEventListener(name, fn)
return e
},
offAll(e) {
if ($.isNodeList(e)) e.forEach(_ => { $.offAll(_) })
if ($.isNode(e)) e.parentNode.replaceChild(e.cloneNode(true), e)
return e
},
// Easy alternative to off(). Disables click, key, submit events.
disable(e) {
if ($.isNodeList(e)) e.forEach(_ => { $.disable(_) })
if ($.isNode(e)) e.disabled = true
return e
},
// For reversing disable()
enable(e) {
if ($.isNodeList(e)) e.forEach(_ => { $.enable(_) })
if ($.isNode(e)) e.disabled = false
return e
},
// Send / trigger event.
// ✂️ Vanilla: Events Dispatch: document.querySelector(".thing").dispatchEvent(new Event('click'))
send(e, name, detail=null) {
if ($.isNodeList(e)) e.forEach(_ => { $.send(_, name, detail) })
if ($.isNode(e)) {
const event = new CustomEvent(name, { detail: detail, bubbles: true })
e.dispatchEvent(event)
}
return e
},
// Halt event. Default: Stops normal event actions and event propagation.
halt(ev, keepBubbling=false, keepDefault=false) {
if (ev instanceof Event) {
if(!keepDefault) ev.preventDefault()
if(!keepBubbling) ev.stopPropagation()
}
return ev
},
// Add or remove attributes from element(s)
attribute(e, name, value=undefined) {
// Get. (Format: "name", "value") Special: Ends call chain.
if (typeof name === 'string' && value === undefined) {
if ($.isNodeList(e)) return [] // Not supported for Get. For many elements, wrap attribute() in any(...).run(...) or any(...).forEach(...)
if ($.isNode(e)) return e.getAttribute(name)
return null // No value. Ends call chain.
}
// Remove.
if (typeof name === 'string' && value === null) {
if ($.isNodeList(e)) e.forEach(_ => { $.attribute(_, name, value) })
if ($.isNode(e)) e.removeAttribute(name)
return e
}
// Add / Set.
if (typeof name === 'string') {
if ($.isNodeList(e)) e.forEach(_ => { $.attribute(_, name, value) })
if ($.isNode(e)) e.setAttribute(name, value)
return e
}
// Format: { "name": "value", "blah": true }
if (typeof name === 'object') {
if ($.isNodeList(e)) e.forEach(_ => { Object.entries(name).forEach(([key, val]) => { $.attribute(_, key, val) }) })
if ($.isNode(e)) Object.entries(name).forEach(([key, val]) => { $.attribute(e, key, val) })
return e
}
return e
},
// Puts Surreal functions except for "restricted" in global scope.
globalsAdd() {
console.log(`Surreal: Adding convenience globals to window.`)
let restricted = ['$', 'sugar']
for (const [key, value] of Object.entries(this)) {
if (!restricted.includes(key)) window[key] != 'undefined' ? window[key] = value : console.warn(`Surreal: "${key}()" already exists on window. Skipping to prevent overwrite.`)
window.document[key] = value
}
},
// ⚙️ Used internally. Is this an element / node?
isNode(e) {
return (e instanceof HTMLElement || e instanceof SVGElement) ? true : false
},
// ⚙️ Used internally by DOM functions. Is this a list of elements / nodes?
isNodeList(e) {
return (e instanceof NodeList || Array.isArray(e)) ? true : false
},
// ⚙️ Used internally by DOM functions. Warning when selector is invalid. Likely missing a "#" or "."
isSelector(selector="", start=document, warning=true) {
if(typeof selector !== 'string') return false
if (start.querySelector(selector) == null) {
if (warning) console.warn(`Surreal: "${selector}" was not found. Missing a character? (. #)`)
return false
}
return true // Valid.
},
}
// Finish up...
$.globalsAdd() // Full convenience.
console.log("Surreal: Loaded.")
return $
})() // End of Surreal 👏
// 🔌 Plugin: Effects
function pluginEffects(e) {
// Fade out and remove element.
// Equivalent to jQuery fadeOut(), but actually removes the element!
function fadeOut(e, fn=false, ms=1000, remove=true) {
let thing = e
if (surreal.isNodeList(e)) e.forEach(_ => { fadeOut(_, fn, ms) })
if (surreal.isNode(e)) {
(async() => {
surreal.styles(e, {transform: 'scale(1)', transition: `all ${ms}ms ease-out`, overflow: 'hidden'})
await tick()
surreal.styles(e, {transform: 'scale(0.9)', opacity: '0'})
await sleep(ms, e)
if (typeof fn === 'function') fn(thing) // Run custom callback?
if (remove) surreal.remove(thing) // Remove element after animation is completed?
})()
}
}
// Fade in an element that has opacity under 1
function fadeIn(e, fn=false, ms=1000) {
let thing = e
if(surreal.isNodeList(e)) e.forEach(_ => { fadeIn(_, fn, ms) })
if(surreal.isNode(e)) {
(async() => {
let save = e.style // Store original style.
surreal.styles(e, {transition: `all ${ms}ms ease-in`, overflow: 'hidden'})
await tick()
surreal.styles(e, {opacity: '1'})
await sleep(ms, e)
e.style = save // Revert back to original style.
surreal.styles(e, {opacity: '1'}) // Ensure we're visible after reverting to original style.
if (typeof fn === 'function') fn(thing) // Run custom callback?
})()
}
}
// Add sugar
e.fadeOut = (fn, ms, remove) => { return fadeOut(e, fn, ms, remove) }
e.fade_out = e.fadeOut
e.fadeIn = (fn, ms) => { return fadeIn(e, fn, ms) }
e.fade_in = e.fadeIn
}
// 🔌 Add plugins here!
surreal.plugins.push(pluginEffects)
console.log("Surreal: Added plugins.")
// 🌐 Add global shortcuts here!
// DOM.
const createElement = create_element = document.createElement.bind(document)
// Animation.
const rAF = typeof requestAnimationFrame !== 'undefined' && requestAnimationFrame
const rIC = typeof requestIdleCallback !== 'undefined' && requestIdleCallback
// Animation: Wait for next animation frame, non-blocking.
async function tick() {
return await new Promise(resolve => { requestAnimationFrame(resolve) })
}
// Animation: Sleep, non-blocking.
async function sleep(ms, e) {
return await new Promise(resolve => setTimeout(() => { resolve(e) }, ms))
}
// Loading: Why? So you don't clobber window.onload (predictable sequential loading)
// Example: <script>onloadAdd(() => { console.log("Page was loaded!") })</script>
// Example: <script>onloadAdd(() => { console.log("Lets do another thing without clobbering window.onload!") })</script>
const onloadAdd = addOnload = onload_add = add_onload = (f) => {
if (typeof window.onload === 'function') { // window.onload already is set, queue functions together (creates a call chain).
let onload_old = window.onload
window.onload = () => {
onload_old()
f()
}
return
}
window.onload = f // window.onload was not set yet.
}
console.log("Surreal: Added shortcuts.")
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.") |
313 |