Skip to content

Commit

Permalink
Added new attribute-tools extension
Browse files Browse the repository at this point in the history
  • Loading branch information
jamcole committed Aug 1, 2024
1 parent 741b082 commit 50e1d50
Show file tree
Hide file tree
Showing 11 changed files with 3,076 additions and 0 deletions.
28 changes: 28 additions & 0 deletions .github/workflows/attribute-tools.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Tests - attribute-tools

on:
push:
branches: [ main ]
paths:
- 'src/attribute-tools/**'
- '.github/workflows/attribute-tools.yml'
pull_request:
branches: [ main ]
paths:
- 'src/attribute-tools/**'
- '.github/workflows/attribute-tools.yml'
workflow_dispatch:

jobs:
test_suite:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
- run: npm ci
working-directory: ./src/attribute-tools
- run: npm test
working-directory: ./src/attribute-tools
1 change: 1 addition & 0 deletions src/attribute-tools/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/test
14 changes: 14 additions & 0 deletions src/attribute-tools/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
BSD Zero Clause License

Copyright (c) 2023, Alexander Petros

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
49 changes: 49 additions & 0 deletions src/attribute-tools/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# `attribute-tools` - Set or Remove Attributes

## Manipulate attributes of an element (e.g. for attributes like "open", "aria-busy", "progress[value]")

The `attribute-tools` extension allows you to specify attributes that will be set or removed from elements by using
an `attribute` or `data-attribute` attribute.

An `attributes` attribute value consists of "runs", which are separated by an `&` character. All
attributes operations within a given run will be applied sequentially, with the delay specified.

Within a run, a `,` character separates distinct attribute operations.

An attribute operation is an operation name `set` or `remove`, followed by an attribute name,
optionally followed by an equal `=` and a value, optionally followed by a colon `:` and a time delay (e.g. 1s).

## Out-of-band attribute manipulation

There is also the option to use `apply-parent-attributes`, or `data-apply-parent-attributes`, which take the same format as `attributes`
but is instead designed for out-of-band updates, allowing you to manipulate attributes of an existing element in the DOM
without otherwise knowing or altering its state.

Any element with this property will schedule attributes to be applied to its _parent_ element, _removing_ itself afterwards,
so it should ideally be used as part of an `hx-swap-oob="beforeend: #some-element"` to add them to the end of the target element.

## Install

```html
<script src="https://unpkg.com/htmx-ext-attribute-tools@2.0.1/attribute-tools.js"></script>
```

## Usage

```html
<!-- The following DOM has attributes swapped in and out as scheduled -->
<div hx-ext="attribute-tools">
<div attributes="add foo=bar"/> <!-- adds the attribute "foo" with the value "bar" after 0ms -->
<div bar="baz" attributes="remove bar:1s"/> <!-- removes the atttribute "bar" after 1s -->
<div bar="baz" foo="blah" attributes="remove bar:1s, set foo=hello:1s, set open:1s"/> <!-- removes the attribute "bar" after 1s
then sets the attribute "foo" to "hello" 1s after that then sets the attribute "open" 1s after that -->
<div bar="baz" attributes="remove bar:1s & set foo:1s"/> <!-- removes the attribute "bar" and adds
attribute "foo" after 1s -->
</div>

<!-- The following OOB update surgically applies attributes to "my-element" -->
<div hx-swap-oob="beforeend: #my-element">
<div hx-ext="attribute-tools"
apply-parent-attribute="add foo, remove foo:10s"/> <!-- adds the attribute "foo" to "my-element" for 10s -->
</div>
```
107 changes: 107 additions & 0 deletions src/attribute-tools/attribute-tools.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
(function() {
function splitOnWhitespace(trigger) {
return trigger.split(/\s+/)
}

function parseAttributeOperation(trimmedValue) {
var split = splitOnWhitespace(trimmedValue)
if (split.length > 1) {
var operation = split[0]
var attributeDef = split[1].trim()
var attribute
var delay
if (attributeDef.indexOf(':') > 0) {
var splitAttribute = attributeDef.split(':')
attribute = splitAttribute[0]
delay = htmx.parseInterval(splitAttribute[1])
} else {
attribute = attributeDef
delay = 0
}
return {
operation,
attribute,
delay
}
} else {
return null
}
}

function setOperation(elt, attributeOperation, attributeList, currentRunTime) {
setTimeout(function() {
if (attributeOperation) {
var split = attributeOperation.attribute.split(/=/)
var name = split[0]
var value = split.length > 1 ? split[1] : ''
elt.setAttribute(name, value)
}

}, currentRunTime)
}

function removeOperation(elt, attributeOperation, attributeList, currentRunTime) {
setTimeout(function() {
if (attributeOperation) {
var split = attributeOperation.attribute.split(/=/)
var name = split[0]
var value = split.length > 1 ? split[1] : ''
elt.removeAttribute(name)
}

}, currentRunTime)
}

function processAttributeList(elt, attributeList) {
var runs = attributeList.split('&')
for (var i = 0; i < runs.length; i++) {
var run = runs[i]
var currentRunTime = 0
var attributeOperations = run.split(',')
for (var j = 0; j < attributeOperations.length; j++) {
var value = attributeOperations[j]
var trimmedValue = value.trim()
var attributeOperation = parseAttributeOperation(trimmedValue)
if (attributeOperation) {
if (attributeOperation.operation === 'remove') {
currentRunTime = currentRunTime + attributeOperation.delay
removeOperation(elt, attributeOperation, attributeList, currentRunTime)
} else if (attributeOperation.operation === 'set') {
currentRunTime = currentRunTime + attributeOperation.delay
setOperation(elt, attributeOperation, attributeList, currentRunTime)
}
}
}
}
}

function maybeProcessAttributes(elt) {
if (elt.getAttribute) {
var attributeList = elt.getAttribute('attributes') || elt.getAttribute('data-attributes')
if (attributeList) {
processAttributeList(elt, attributeList)
}
}
}

htmx.defineExtension('attribute-tools', {
onEvent: function(name, evt) {
if (name === 'htmx:afterProcessNode') {
var elt = evt.detail.elt
maybeProcessAttributes(elt)
var attributeList = elt.getAttribute("apply-parent-attributes") || elt.getAttribute("data-apply-parent-attributes");
if (attributeList) {
var parent = elt.parentElement;
parent.removeChild(elt);
parent.setAttribute("data-attributes", attributeList);
maybeProcessAttributes(parent);
} else if (elt.querySelectorAll) {
var children = elt.querySelectorAll('[attributes], [data-attributes]')
for (var i = 0; i < children.length; i++) {
maybeProcessAttributes(children[i])
}
}
}
}
})
})()
Loading

0 comments on commit 50e1d50

Please sign in to comment.