Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experiment with tree traversal and new style 'me' #6

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 63 additions & 18 deletions surreal.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,37 +36,65 @@ var $ = { // You can use a different name than "$", but you must change the refe
e.attributes = e.attribute
e.attr = e.attribute

// Tree Traversal.
e.el = (selector, start = e, warning = true) => $.el(selector, start, warning)
e.any = (selector, start = e, warning = true) => $.any(selector, start, warning)
Object.defineProperty(e, 'parent', {
get: () => $.sugar(e.parentElement),
configurable: true
})


// Add all plugin sugar.
$._e = e // Plugin access to "e" for chaining.
for (const [key, value] of Object.entries(sugars)) {
for (const [key, value] of Object.entries($.sugars)) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't figure out what changed, but at some point this was giving me an error that sugars was undefined until I added the $..

e[key] = value.bind($) //e[key] = value
}

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.
// Return current element. Works inside <script> or event handlers created with surreal 🔥
// Example
// <div>
// Hello World!
// <script>me().style.color = 'red'</script>
// <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 $.me(selector.target) // Events return event.target
get me() {
if (document.currentScript) return $.sugar(document.currentScript.parentElement) // Just local me in <script>
else return $._evtEl // If we are in an event handler this will be the element declaring the handler
},

// Return single element.
// If your query returns a collection, it will return the first element.
el(selector, start=document, warning=true) {
if ($.isNodeList(start)) {
for (const node of start) {
const res = $.el(selector, node, warning)
if (res) return res
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is the best way to chain el on top of any. Right now it just returns the first match, to keep things in line such that even when chained, el only returns one item.

}
return null
}
if (selector instanceof Event) return $.el(selector.currentTarget) // Events return event.currentTarget
if (typeof selector == 'string' && 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 ($.isNodeList(selector)) return $.el(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.
// any() is el() 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 $.any(selector.target) // Events return event.target
if ($.isNodeList(start)) {
let res = []
for (const node of start) {
const matches = $.any(selector, node, false)
if (matches) res = res.concat(matches)
}
return [...new Set(res)] // Remove duplicate matches
}
if (selector instanceof Event) return $.any(selector.currentTarget) // Events return event.currentTarget
if (typeof selector == 'string' && isSelector(selector, start, true, 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.
Expand Down Expand Up @@ -138,8 +166,14 @@ var $ = { // You can use a different name than "$", but you must change the refe
// ✂️ 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)
// Store the current element for use by 'me' in the event handler
function listener(evt) {
$._evtEl = e
try { fn(evt) }
finally { delete $._evtEl }
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No idea if this is the best way to store the current element for an event handler, but it's the only way I could figure out to get me working without needing the event as a parameter. I don't like that it doesn't work for vanilla JS registered handlers, but el(event) still works in that case.

if ($.isNodeList(e)) e.forEach(_ => { on(_, name, listener) })
if ($.isNode(e)) e.addEventListener(name, listener)
return e
},

Expand Down Expand Up @@ -211,10 +245,11 @@ var $ = { // You can use a different name than "$", but you must change the refe
// Puts Surreal functions except for "restricted" in global scope.
globalsAdd() {
console.log(`Surreal: adding convenience globals to window`)
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
restricted = ['sugar']
for (const key of Object.keys(this)) {
const descriptor = Object.getOwnPropertyDescriptor(this, key)
if (!restricted.includes(key) && !key.startsWith('$')) window[key] != 'undefined' ? Object.defineProperty(window, key, descriptor) : console.warn(`Surreal: "${key}()" already exists on window. Skipping to prevent overwrite.`)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I switched this to startsWith('$') because $effects was giving me a problem when switching to the descriptor mode. Could just be that I need to switch to configurable: true on the descriptors so they can be overwritten.

Object.defineProperty(window.document, key, descriptor)
}
},

Expand Down Expand Up @@ -242,6 +277,16 @@ var $ = { // You can use a different name than "$", but you must change the refe
},
}

function mergePlugin(dollarsign, plugin) {
// Merges properties of plugin into dollarsign, keeping descriptors intact
const props = Object.keys(plugin)
for (const prop of props) {
const descriptor = Object.getOwnPropertyDescriptor(plugin, prop)
Object.defineProperty(dollarsign, prop, descriptor)
}
return dollarsign
}

// 📦 Plugin: Effects
var $effects = {
// Fade out and remove element.
Expand Down Expand Up @@ -278,7 +323,7 @@ var $effects = {
},
$effects
}
$ = {...$, ...$effects}
mergePlugin($, $effects)
$.sugars['fadeOut'] = (fn, ms) => { return $.fadeOut($._e, fn=false, ms=1000) }
$.sugars['fadeIn'] = (fn, ms) => { return $.fadeIn($._e, fn=false, ms=1000) }
$.sugars['fade_out', 'fade_in'] = $.sugars['fadeOut', 'fadeIn']
Expand Down