From 8ae63d96b6ce8b27b1c7390eb49a6ba3a8a56a3d Mon Sep 17 00:00:00 2001 From: carson Date: Mon, 17 Jan 2022 13:10:32 -0700 Subject: [PATCH 1/2] add alt attributes to images fixes https://github.com/bigskysoftware/htmx/issues/742 --- src/htmx.js | 111 +++++++++++++----- test/attributes/hx-sync.js | 224 +++++++++++++++++++++++++++++++++++++ test/index.html | 1 + 3 files changed, 306 insertions(+), 30 deletions(-) create mode 100644 test/attributes/hx-sync.js diff --git a/src/htmx.js b/src/htmx.js index 1385a631..bb325acd 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -1063,7 +1063,7 @@ return (function () { triggerSpec.delay = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA)); } else if (token === "from" && tokens[0] === ":") { tokens.shift(); - let from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA); + var from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA); if (from_arg === "closest" || from_arg === "find") { tokens.shift(); from_arg += @@ -2226,42 +2226,87 @@ return (function () { return; // do not issue requests for elements removed from the DOM } var target = etc.targetOverride || getTarget(elt); - if (target == null) { + if (target == null || target == DUMMY_ELT) { triggerErrorEvent(elt, 'htmx:targetError', {target: getAttributeValue(elt, "hx-target")}); return; } + + var syncElt = elt; var eltData = getInternalData(elt); - if (eltData.requestInFlight) { - var queueStrategy = 'last'; - if (event) { - var eventData = getInternalData(event); - if (eventData && eventData.triggerSpec && eventData.triggerSpec.queue) { - queueStrategy = eventData.triggerSpec.queue; + var syncStrategy = getClosestAttributeValue(elt, "hx-sync"); + var queueStrategy = null; + var abortable = false; + if (syncStrategy) { + var syncStrings = syncStrategy.split(":"); + var selector = syncStrings[0].trim(); + if (selector === "this") { + syncElt = getClosestMatch(elt, function (elt) { + return getAttributeValue(elt, "hx-sync") != null; + }); + } else { + syncElt = querySelectorExt(elt, selector); + } + // default to the drop strategy + syncStrategy = (syncStrings[1] || 'drop').trim(); + eltData = getInternalData(syncElt); + if (syncStrategy === "drop" && eltData.xhr && eltData.abortable !== true) { + return; + } else if (syncStrategy === "abort") { + if (eltData.xhr) { + return; + } else { + abortable = true; } + } else if (syncStrategy === "replace") { + triggerEvent(syncElt, 'htmx:abort'); // abort the current request and continue + } else if (syncStrategy.indexOf("queue") === 0) { + var queueStrArray = syncStrategy.split(" "); + queueStrategy = (queueStrArray[1] || "last").trim(); } - if (eltData.queuedRequests == null) { - eltData.queuedRequests = []; - } - if (queueStrategy === "first" && eltData.queuedRequests.length === 0) { - eltData.queuedRequests.push(function () { - issueAjaxRequest(verb, path, elt, event, etc) - }); - } else if (queueStrategy === "all") { - eltData.queuedRequests.push(function () { - issueAjaxRequest(verb, path, elt, event, etc) - }); - } else if (queueStrategy === "last") { - eltData.queuedRequests = []; // dump existing queue - eltData.queuedRequests.push(function () { - issueAjaxRequest(verb, path, elt, event, etc) - }); - } - return; - } else { - eltData.requestInFlight = true; } + + if (eltData.xhr) { + if (eltData.abortable) { + triggerEvent(syncElt, 'htmx:abort'); // abort the current request and continue + } else { + if(queueStrategy == null){ + if (event) { + var eventData = getInternalData(event); + if (eventData && eventData.triggerSpec && eventData.triggerSpec.queue) { + queueStrategy = eventData.triggerSpec.queue; + } + } + if (queueStrategy == null) { + queueStrategy = "last"; + } + } + if (eltData.queuedRequests == null) { + eltData.queuedRequests = []; + } + if (queueStrategy === "first" && eltData.queuedRequests.length === 0) { + eltData.queuedRequests.push(function () { + issueAjaxRequest(verb, path, elt, event, etc) + }); + } else if (queueStrategy === "all") { + eltData.queuedRequests.push(function () { + issueAjaxRequest(verb, path, elt, event, etc) + }); + } else if (queueStrategy === "last") { + eltData.queuedRequests = []; // dump existing queue + eltData.queuedRequests.push(function () { + issueAjaxRequest(verb, path, elt, event, etc) + }); + } + return; + } + } + + var xhr = new XMLHttpRequest(); + eltData.xhr = xhr; + eltData.abortable = abortable; var endRequestLock = function(){ - eltData.requestInFlight = false + eltData.xhr = null; + eltData.abortable = false; if (eltData.queuedRequests != null && eltData.queuedRequests.length > 0) { var queuedRequest = eltData.queuedRequests.shift(); @@ -2289,7 +2334,6 @@ return (function () { } } - var xhr = new XMLHttpRequest(); var headers = getHeaders(elt, target, promptResponse); if (etc.headers) { @@ -2760,6 +2804,13 @@ return (function () { insertIndicatorStyles(); var body = getDocument().body; processNode(body); + body.addEventListener("htmx:abort", function (evt) { + var target = evt.target; + var internalData = getInternalData(target); + if (internalData && internalData.xhr) { + internalData.xhr.abort(); + } + }); window.onpopstate = function (event) { if (event.state && event.state.htmx) { restoreHistory(); diff --git a/test/attributes/hx-sync.js b/test/attributes/hx-sync.js new file mode 100644 index 00000000..6f4d0d10 --- /dev/null +++ b/test/attributes/hx-sync.js @@ -0,0 +1,224 @@ +describe("hx-sync attribute", function(){ + beforeEach(function() { + this.server = makeServer(); + clearWorkArea(); + }); + afterEach(function() { + this.server.restore(); + clearWorkArea(); + }); + + it('can use drop strategy', function() + { + var count = 0; + this.server.respondWith("GET", "/test", function(xhr){ + xhr.respond(200, {}, "Click " + count++); + }); + make('
' + + '
') + var b1 = byId("b1"); + var b2 = byId("b2"); + b1.click(); + b2.click(); + this.server.respond(); + this.server.respond(); + b1.innerHTML.should.equal('Click 0'); + b2.innerHTML.should.equal('Initial'); + }); + + it('defaults to the drop strategy', function() + { + var count = 0; + this.server.respondWith("GET", "/test", function(xhr){ + xhr.respond(200, {}, "Click " + count++); + }); + make('
' + + '
') + var b1 = byId("b1"); + var b2 = byId("b2"); + b1.click(); + b2.click(); + this.server.respond(); + this.server.respond(); + b1.innerHTML.should.equal('Click 0'); + b2.innerHTML.should.equal('Initial'); + }); + + it('can use replace strategy', function() + { + var count = 0; + this.server.respondWith("GET", "/test", function(xhr){ + xhr.respond(200, {}, "Click " + count++); + }); + make('
' + + '
') + var b1 = byId("b1"); + var b2 = byId("b2"); + b1.click(); + b2.click(); + this.server.respond(); + this.server.respond(); + b1.innerHTML.should.equal('Initial'); + b2.innerHTML.should.equal('Click 0'); + }); + + it('can use queue all strategy', function() + { + var count = 0; + this.server.respondWith("GET", "/test", function(xhr){ + xhr.respond(200, {}, "Click " + count++); + }); + make('
' + + ' ' + + '
') + var b1 = byId("b1"); + b1.click(); + + var b2 = byId("b2"); + b2.click(); + + var b3 = byId("b3"); + b3.click(); + + this.server.respond(); + b1.innerHTML.should.equal('Click 0'); + b2.innerHTML.should.equal('Initial'); + b3.innerHTML.should.equal('Initial'); + + this.server.respond(); + b1.innerHTML.should.equal('Click 0'); + b2.innerHTML.should.equal('Click 1'); + b3.innerHTML.should.equal('Initial'); + + this.server.respond(); + b1.innerHTML.should.equal('Click 0'); + b2.innerHTML.should.equal('Click 1'); + b3.innerHTML.should.equal('Click 2'); + }); + + it('can use queue last strategy', function() + { + var count = 0; + this.server.respondWith("GET", "/test", function(xhr){ + xhr.respond(200, {}, "Click " + count++); + }); + make('
' + + ' ' + + '
') + var b1 = byId("b1"); + b1.click(); + + var b2 = byId("b2"); + b2.click(); + + var b3 = byId("b3"); + b3.click(); + + this.server.respond(); + b1.innerHTML.should.equal('Click 0'); + b2.innerHTML.should.equal('Initial'); + b3.innerHTML.should.equal('Initial'); + + this.server.respond(); + b1.innerHTML.should.equal('Click 0'); + b2.innerHTML.should.equal('Initial'); + b3.innerHTML.should.equal('Click 1'); + + this.server.respond(); + b1.innerHTML.should.equal('Click 0'); + b2.innerHTML.should.equal('Initial'); + b3.innerHTML.should.equal('Click 1'); + }); + + it('can use queue first strategy', function() + { + var count = 0; + this.server.respondWith("GET", "/test", function(xhr){ + xhr.respond(200, {}, "Click " + count++); + }); + make('
' + + ' ' + + '
') + var b1 = byId("b1"); + b1.click(); + + var b2 = byId("b2"); + b2.click(); + + var b3 = byId("b3"); + b3.click(); + + this.server.respond(); + b1.innerHTML.should.equal('Click 0'); + b2.innerHTML.should.equal('Initial'); + b3.innerHTML.should.equal('Initial'); + + this.server.respond(); + b1.innerHTML.should.equal('Click 0'); + b2.innerHTML.should.equal('Click 1'); + b3.innerHTML.should.equal('Initial'); + + this.server.respond(); + b1.innerHTML.should.equal('Click 0'); + b2.innerHTML.should.equal('Click 1'); + b3.innerHTML.should.equal('Initial'); + }); + + it('can use abort strategy to end existing abortable request', function() + { + var count = 0; + this.server.respondWith("GET", "/test", function(xhr){ + xhr.respond(200, {}, "Click " + count++); + }); + make('
' + + '
') + var b1 = byId("b1"); + var b2 = byId("b2"); + b1.click(); + b2.click(); + this.server.respond(); + this.server.respond(); + b1.innerHTML.should.equal('Initial'); + b2.innerHTML.should.equal('Click 0'); + }); + + it('can use abort strategy to drop abortable request when one is in flight', function() + { + var count = 0; + this.server.respondWith("GET", "/test", function(xhr){ + xhr.respond(200, {}, "Click " + count++); + }); + make('
' + + '
') + var b1 = byId("b1"); + var b2 = byId("b2"); + b2.click(); + b1.click(); + this.server.respond(); + this.server.respond(); + b1.innerHTML.should.equal('Initial'); + b2.innerHTML.should.equal('Click 0'); + }); + + it('can abort a request programmatically', function() + { + var count = 0; + this.server.respondWith("GET", "/test", function(xhr){ + xhr.respond(200, {}, "Click " + count++); + }); + make('
' + + '
') + var b1 = byId("b1"); + var b2 = byId("b2"); + b1.click(); + b2.click(); + + htmx.trigger(b1, "htmx:abort"); + + this.server.respond(); + this.server.respond(); + b1.innerHTML.should.equal('Initial'); + b2.innerHTML.should.equal('Click 0'); + }); + +}) diff --git a/test/index.html b/test/index.html index 359156c2..f65cd721 100644 --- a/test/index.html +++ b/test/index.html @@ -73,6 +73,7 @@ + From e6751be2eefbbacd3ff9c5661dba8922e4cfdb6a Mon Sep 17 00:00:00 2001 From: carson Date: Mon, 17 Jan 2022 13:10:32 -0700 Subject: [PATCH 2/2] first pass at `hx-sync` attribute --- src/htmx.js | 111 +++++++++++++----- test/attributes/hx-sync.js | 224 +++++++++++++++++++++++++++++++++++++ test/index.html | 1 + 3 files changed, 306 insertions(+), 30 deletions(-) create mode 100644 test/attributes/hx-sync.js diff --git a/src/htmx.js b/src/htmx.js index 1385a631..bb325acd 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -1063,7 +1063,7 @@ return (function () { triggerSpec.delay = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA)); } else if (token === "from" && tokens[0] === ":") { tokens.shift(); - let from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA); + var from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA); if (from_arg === "closest" || from_arg === "find") { tokens.shift(); from_arg += @@ -2226,42 +2226,87 @@ return (function () { return; // do not issue requests for elements removed from the DOM } var target = etc.targetOverride || getTarget(elt); - if (target == null) { + if (target == null || target == DUMMY_ELT) { triggerErrorEvent(elt, 'htmx:targetError', {target: getAttributeValue(elt, "hx-target")}); return; } + + var syncElt = elt; var eltData = getInternalData(elt); - if (eltData.requestInFlight) { - var queueStrategy = 'last'; - if (event) { - var eventData = getInternalData(event); - if (eventData && eventData.triggerSpec && eventData.triggerSpec.queue) { - queueStrategy = eventData.triggerSpec.queue; + var syncStrategy = getClosestAttributeValue(elt, "hx-sync"); + var queueStrategy = null; + var abortable = false; + if (syncStrategy) { + var syncStrings = syncStrategy.split(":"); + var selector = syncStrings[0].trim(); + if (selector === "this") { + syncElt = getClosestMatch(elt, function (elt) { + return getAttributeValue(elt, "hx-sync") != null; + }); + } else { + syncElt = querySelectorExt(elt, selector); + } + // default to the drop strategy + syncStrategy = (syncStrings[1] || 'drop').trim(); + eltData = getInternalData(syncElt); + if (syncStrategy === "drop" && eltData.xhr && eltData.abortable !== true) { + return; + } else if (syncStrategy === "abort") { + if (eltData.xhr) { + return; + } else { + abortable = true; } + } else if (syncStrategy === "replace") { + triggerEvent(syncElt, 'htmx:abort'); // abort the current request and continue + } else if (syncStrategy.indexOf("queue") === 0) { + var queueStrArray = syncStrategy.split(" "); + queueStrategy = (queueStrArray[1] || "last").trim(); } - if (eltData.queuedRequests == null) { - eltData.queuedRequests = []; - } - if (queueStrategy === "first" && eltData.queuedRequests.length === 0) { - eltData.queuedRequests.push(function () { - issueAjaxRequest(verb, path, elt, event, etc) - }); - } else if (queueStrategy === "all") { - eltData.queuedRequests.push(function () { - issueAjaxRequest(verb, path, elt, event, etc) - }); - } else if (queueStrategy === "last") { - eltData.queuedRequests = []; // dump existing queue - eltData.queuedRequests.push(function () { - issueAjaxRequest(verb, path, elt, event, etc) - }); - } - return; - } else { - eltData.requestInFlight = true; } + + if (eltData.xhr) { + if (eltData.abortable) { + triggerEvent(syncElt, 'htmx:abort'); // abort the current request and continue + } else { + if(queueStrategy == null){ + if (event) { + var eventData = getInternalData(event); + if (eventData && eventData.triggerSpec && eventData.triggerSpec.queue) { + queueStrategy = eventData.triggerSpec.queue; + } + } + if (queueStrategy == null) { + queueStrategy = "last"; + } + } + if (eltData.queuedRequests == null) { + eltData.queuedRequests = []; + } + if (queueStrategy === "first" && eltData.queuedRequests.length === 0) { + eltData.queuedRequests.push(function () { + issueAjaxRequest(verb, path, elt, event, etc) + }); + } else if (queueStrategy === "all") { + eltData.queuedRequests.push(function () { + issueAjaxRequest(verb, path, elt, event, etc) + }); + } else if (queueStrategy === "last") { + eltData.queuedRequests = []; // dump existing queue + eltData.queuedRequests.push(function () { + issueAjaxRequest(verb, path, elt, event, etc) + }); + } + return; + } + } + + var xhr = new XMLHttpRequest(); + eltData.xhr = xhr; + eltData.abortable = abortable; var endRequestLock = function(){ - eltData.requestInFlight = false + eltData.xhr = null; + eltData.abortable = false; if (eltData.queuedRequests != null && eltData.queuedRequests.length > 0) { var queuedRequest = eltData.queuedRequests.shift(); @@ -2289,7 +2334,6 @@ return (function () { } } - var xhr = new XMLHttpRequest(); var headers = getHeaders(elt, target, promptResponse); if (etc.headers) { @@ -2760,6 +2804,13 @@ return (function () { insertIndicatorStyles(); var body = getDocument().body; processNode(body); + body.addEventListener("htmx:abort", function (evt) { + var target = evt.target; + var internalData = getInternalData(target); + if (internalData && internalData.xhr) { + internalData.xhr.abort(); + } + }); window.onpopstate = function (event) { if (event.state && event.state.htmx) { restoreHistory(); diff --git a/test/attributes/hx-sync.js b/test/attributes/hx-sync.js new file mode 100644 index 00000000..6f4d0d10 --- /dev/null +++ b/test/attributes/hx-sync.js @@ -0,0 +1,224 @@ +describe("hx-sync attribute", function(){ + beforeEach(function() { + this.server = makeServer(); + clearWorkArea(); + }); + afterEach(function() { + this.server.restore(); + clearWorkArea(); + }); + + it('can use drop strategy', function() + { + var count = 0; + this.server.respondWith("GET", "/test", function(xhr){ + xhr.respond(200, {}, "Click " + count++); + }); + make('
' + + '
') + var b1 = byId("b1"); + var b2 = byId("b2"); + b1.click(); + b2.click(); + this.server.respond(); + this.server.respond(); + b1.innerHTML.should.equal('Click 0'); + b2.innerHTML.should.equal('Initial'); + }); + + it('defaults to the drop strategy', function() + { + var count = 0; + this.server.respondWith("GET", "/test", function(xhr){ + xhr.respond(200, {}, "Click " + count++); + }); + make('
' + + '
') + var b1 = byId("b1"); + var b2 = byId("b2"); + b1.click(); + b2.click(); + this.server.respond(); + this.server.respond(); + b1.innerHTML.should.equal('Click 0'); + b2.innerHTML.should.equal('Initial'); + }); + + it('can use replace strategy', function() + { + var count = 0; + this.server.respondWith("GET", "/test", function(xhr){ + xhr.respond(200, {}, "Click " + count++); + }); + make('
' + + '
') + var b1 = byId("b1"); + var b2 = byId("b2"); + b1.click(); + b2.click(); + this.server.respond(); + this.server.respond(); + b1.innerHTML.should.equal('Initial'); + b2.innerHTML.should.equal('Click 0'); + }); + + it('can use queue all strategy', function() + { + var count = 0; + this.server.respondWith("GET", "/test", function(xhr){ + xhr.respond(200, {}, "Click " + count++); + }); + make('
' + + ' ' + + '
') + var b1 = byId("b1"); + b1.click(); + + var b2 = byId("b2"); + b2.click(); + + var b3 = byId("b3"); + b3.click(); + + this.server.respond(); + b1.innerHTML.should.equal('Click 0'); + b2.innerHTML.should.equal('Initial'); + b3.innerHTML.should.equal('Initial'); + + this.server.respond(); + b1.innerHTML.should.equal('Click 0'); + b2.innerHTML.should.equal('Click 1'); + b3.innerHTML.should.equal('Initial'); + + this.server.respond(); + b1.innerHTML.should.equal('Click 0'); + b2.innerHTML.should.equal('Click 1'); + b3.innerHTML.should.equal('Click 2'); + }); + + it('can use queue last strategy', function() + { + var count = 0; + this.server.respondWith("GET", "/test", function(xhr){ + xhr.respond(200, {}, "Click " + count++); + }); + make('
' + + ' ' + + '
') + var b1 = byId("b1"); + b1.click(); + + var b2 = byId("b2"); + b2.click(); + + var b3 = byId("b3"); + b3.click(); + + this.server.respond(); + b1.innerHTML.should.equal('Click 0'); + b2.innerHTML.should.equal('Initial'); + b3.innerHTML.should.equal('Initial'); + + this.server.respond(); + b1.innerHTML.should.equal('Click 0'); + b2.innerHTML.should.equal('Initial'); + b3.innerHTML.should.equal('Click 1'); + + this.server.respond(); + b1.innerHTML.should.equal('Click 0'); + b2.innerHTML.should.equal('Initial'); + b3.innerHTML.should.equal('Click 1'); + }); + + it('can use queue first strategy', function() + { + var count = 0; + this.server.respondWith("GET", "/test", function(xhr){ + xhr.respond(200, {}, "Click " + count++); + }); + make('
' + + ' ' + + '
') + var b1 = byId("b1"); + b1.click(); + + var b2 = byId("b2"); + b2.click(); + + var b3 = byId("b3"); + b3.click(); + + this.server.respond(); + b1.innerHTML.should.equal('Click 0'); + b2.innerHTML.should.equal('Initial'); + b3.innerHTML.should.equal('Initial'); + + this.server.respond(); + b1.innerHTML.should.equal('Click 0'); + b2.innerHTML.should.equal('Click 1'); + b3.innerHTML.should.equal('Initial'); + + this.server.respond(); + b1.innerHTML.should.equal('Click 0'); + b2.innerHTML.should.equal('Click 1'); + b3.innerHTML.should.equal('Initial'); + }); + + it('can use abort strategy to end existing abortable request', function() + { + var count = 0; + this.server.respondWith("GET", "/test", function(xhr){ + xhr.respond(200, {}, "Click " + count++); + }); + make('
' + + '
') + var b1 = byId("b1"); + var b2 = byId("b2"); + b1.click(); + b2.click(); + this.server.respond(); + this.server.respond(); + b1.innerHTML.should.equal('Initial'); + b2.innerHTML.should.equal('Click 0'); + }); + + it('can use abort strategy to drop abortable request when one is in flight', function() + { + var count = 0; + this.server.respondWith("GET", "/test", function(xhr){ + xhr.respond(200, {}, "Click " + count++); + }); + make('
' + + '
') + var b1 = byId("b1"); + var b2 = byId("b2"); + b2.click(); + b1.click(); + this.server.respond(); + this.server.respond(); + b1.innerHTML.should.equal('Initial'); + b2.innerHTML.should.equal('Click 0'); + }); + + it('can abort a request programmatically', function() + { + var count = 0; + this.server.respondWith("GET", "/test", function(xhr){ + xhr.respond(200, {}, "Click " + count++); + }); + make('
' + + '
') + var b1 = byId("b1"); + var b2 = byId("b2"); + b1.click(); + b2.click(); + + htmx.trigger(b1, "htmx:abort"); + + this.server.respond(); + this.server.respond(); + b1.innerHTML.should.equal('Initial'); + b2.innerHTML.should.equal('Click 0'); + }); + +}) diff --git a/test/index.html b/test/index.html index 359156c2..f65cd721 100644 --- a/test/index.html +++ b/test/index.html @@ -73,6 +73,7 @@ +