prologic revised this gist . 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