Skip to content

Instantly share code, notes, and snippets.

@amcgregor
Last active February 8, 2024 17:44
Show Gist options
  • Save amcgregor/b92e981fdcf4a85cb30242581732f07a to your computer and use it in GitHub Desktop.
Save amcgregor/b92e981fdcf4a85cb30242581732f07a to your computer and use it in GitHub Desktop.
Currently untested ES6 notepad sketches of isolatable global behaviors.
{ // "Change delay" committal of altered form fields and editable content without waiting for blur.
function monitorTyping(
element,
initial = 1000, // Initial delay value of one second.
highlight = true, // When entering a field, select its contents.
minimum = 500, // Minimum wait time.
threshold = 2, // Don't bother if the field has fewer than this many characters.
forceable = true // Ignore threshold on blur and when pressing enter.
) {
var timeout,
lastChange = 0,
delay = changeDelayDefaults.initial,
value = element.attributes.contenteditable ? element.innerHTML : element.value
if ( // Only handle contenteditable, <input type="text"> and <textarea> tags.
!element.attributes.contenteditable &&
element.type.toUpperCase() != "TEXT" &&
element.nodeName.toUpperCase() != "TEXTAREA"
) return null;
function handler(forced=false) {
lastChange = 0;
var nvalue = element.attributes.contenteditable ? element.innerHTML : element.value;
// If nothing has changed, or the content length is below our threshold, nothing to do.
if ( !forced && (nvalue == value || nvalue.length < threshold) ) {
value = nvalue
return;
}
let changed = new Event('change')
element.dispatchEvent(changed)
}
function check(e) {
clearTimeout(timeout)
if ( event.keyCode == 13 && this.type.toUpperCase() == "TEXT" ) handler(forceable)
if ( lastChange === 0 ) {
lastChange = Date.now()
delay = initial
timeout = setTimeout(handler, delay)
return
}
}
element.addEventListener('keyup', check)
element.addEventListener('blur', e => handler(forceable))
if ( highlight ) element.addEventListener('focus', e => try {
if ( e.target.attributes.contenteditable ) window.getSelection().selectAllChildren(e.target)
else e.target.select()
} catch (e) {})
return element
}
}
/* Port of https://github.com/amcgregor/change-delay/blob/master/jquery.change-delay.js - requires testing. */
{ // Collapse opened menus if focused activation target clicked again.
for ( let elem of [...document.querySelectorAll('header#top li > label')] )
elem.addEventListener('mousedown', e => {
if ( e.target.matches(':focus') ) {
e.target.blur()
e.preventDefault()
}
})
}
/* Tested as working via https://codepen.io/amcgregor/project/live/9f171b58c35d32fd02f13bef0778b311 */
// As this "component" is slightly more involved, it has its own Gist:
// https://gist.github.com/amcgregor/f3529394f032ae517b0a02a0edbac7f7
{ // Detect scrolling of the viewport away from the absolute top of the document.
function scrollHandler(e) {
if ( window.scrollY == 0 && document.body.scrollTop == 0 ) document.body.classList.remove('offset')
else document.body.classList.add('offset')
}
// Watch for the page being srolled.
window.addEventListener('scroll', scrollHandler)
document.body.addEventListener('scroll', scrollHandler)
}
/* Tested as working via https://codepen.io/amcgregor/project/live/9f171b58c35d32fd02f13bef0778b311 */
{ // Page visibility detection (level) and event (edge) response for user attentiveness inference and detection.
var stateAttribute = null // Document object attribute name holding the current (level) visbility state.
const v = "active", h = "inactive", // Mapping of "visible" and "hidden" levels to CSS class names.
stateMap = { // CSS body element mapping of visibility event (edge) to CSS class name transitioning to.
focus: v, focusin: v, pageshow: v,
blur: h, focusout: h, pagehide: h,
}
function visibilityChanged(e) { // Generalized handler covering both explicit and inferred visibility.
e = e || window.event
document.body.classList.remove(h, v) // Remove both, the now-active state will be added back.
if ( e.type in stateMap ) document.body.classList.add(stateMap[e.type])
else document.body.classList.add(this[stateAttribute] ? h : v)
}
// Attach browser-specific or generic handlers as appropriate.
for ( var prefix of ['moz', 'webkit', 'ms', ''] ) {
let attr = prefix + 'Hidden'
attr = attr[0].toLowerCase() + attr.substr(1)
if ( attr in document ) {
document.addEventListener(prefix + 'visibilitychange', visibilityChanged)
stateAttribute = attr
break
}
}
// Handle all other browsers through inference from available events. Skipped: user activity detection.
if ( stateAttribute === null ) window.onpageshow = window.onpagehide = window.onfocus = window.onblur = visibilityChanged;
// Set initial state if detection is possible by invoking the handler with a synthesized event.
if ( document[stateAttribute] !== undefined ) visibilityChanged({type: document[stateAttribute] ? 'blur' : 'focus'})
}
/* New development, requires testing. */
// As there are many prototype mutations to issue to add Python-like functionality, it has its own Gist:
// https://gist.github.com/amcgregor/0c79746b8391da346c7590ddac6b1881
// Formerly: https://gist.github.com/amcgregor/5b479dbf167a244454780434a12885be
{ // Detect scrolling of the viewport away from the absolute top of the document.
function scrollHandler(e) {
if ( window.scrollY == 0 && document.body.scrollTop == 0 ) document.body.classList.remove('offset')
else document.body.classList.add('offset')
}
// Watch for the page being scrolled.
window.addEventListener('scroll', scrollHandler)
document.body.addEventListener('scroll', scrollHandler)
}
{ // ARIA accessible tabbed interface navigation.
document.body.addEventListener('focus', e => {
let parent = findParent(e.target, '[role=tablist]')
let container = findParent(event.target, 'dd[role=tabpanel]')
if ( !parent || !container ) return // We weren't contained within a tabset or tab.
if ( !container.previousSibling.matches("[aria-selected]") ) container.previousSibling.click()
}, {capture: true}) // Capture on the way down, not on the route back up.
// This should "switch tab" before focusing the element.
for ( let elem of [...document.querySelectorAll('[role=tablist] [role=tab]')] ) {
elem.addEventListener('click', e => {
for ( let sibling of [...e.target.parentElement.querySelectorAll('[aria-selected]')] )
sibling.ariaSelected = undefined
e.target.ariaSelected = true
})
elem.addEventListener('keydown', e => {
var target = null;
switch ( event.key ) {
case ' ':
case 'Enter':
target = event.target
break
case 'ArrowLeft':
case 'ArrowUp':
case 'h': // Vim
case 'k':
case 'w': // Gaming
case 'a':
if ( e.target.ariaSelected )
target = getPreviousElement(event.target, '[role=tab]')
break
case 'ArrowRight':
case 'ArrowDown':
case 'l': // Vim
case 'j':
case 's': // Gaming
case 'd':
if ( e.target.ariaSelected )
target = getNextElement(event.target, '[role=tab]')
break
}
if ( !target ) return;
target.focus()
target.click()
e.preventDefault()
e.stopPropagation()
})
}
}
/* Tested as working via https://codepen.io/amcgregor/pen/yLgdrey */
// My implementation is AKA "Relativistic".
// Present <time> elements as relative to now while preserving the original exact date+time.
// Monitor for element injection and attribute manipulation to populate or update the relative time presentations.
// As time progresses, keep the relative presentations accurate.
// Actual repository: https://github.com/amcgregor/es6-relativistic
// Gist: https://gist.github.com/amcgregor/c675c7ef41156be802976d577c0bd159
{ // In the event the page title is truncated, permit hover tooltip w/o data model duplication.
const pageTitle = document.querySelector('header#top h2')
pageTitle.title = pageTitle.innerText
function titleChanged(m) {
// TODO: Determine if the event is fired pre- or post- update of the document.title
mutations.forEach(function(mutation) {
pageTitle.title = pageTitle.innerText = mutation.target.textContent.split('-')[0].trim()
});
pageTitle.title = pageTitle.innerText = document.title.split('-')[0].trim()
}
const titleObserver = new MutationObserver(titleChanged);
titleObserver.observe(document.head.querySelector('title'), {subtree: true, characterData: true, childList: true})
}
/* Title attribute assignment tested as working via https://codepen.io/amcgregor/project/live/9f171b58c35d32fd02f13bef0778b311 */
/* Observation and reaction to changes requires testing. */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment