From 18220b32831cedb8873552dc5cc8d8bda213353a Mon Sep 17 00:00:00 2001 From: carson Date: Sun, 4 Oct 2020 18:26:17 -0600 Subject: [PATCH] hook in tokenizer implementation --- src/htmx.js | 135 +++++++++++++++++++++------------- test/attributes/hx-trigger.js | 20 +++-- test/core/tokenizer.js | 12 +-- 3 files changed, 106 insertions(+), 61 deletions(-) diff --git a/src/htmx.js b/src/htmx.js index d47800c0..010e6d32 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -366,7 +366,7 @@ return (function () { } else if (targetStr.indexOf("closest ") === 0) { return closest(elt, targetStr.substr(8)); } else if (targetStr.indexOf("find ") === 0) { - return find(elt, targetStr.substr(5)); + return find(elt, targetStr.substr(5)); } else { return getDocument().querySelector(targetStr); } @@ -642,6 +642,7 @@ return (function () { var SYMBOL_START = /[_$a-zA-Z]/; var SYMBOL_CONT = /[_$a-zA-Z0-9]/; var STRINGISH_START = ['"', "'", "/"]; + var NOT_WHITESPACE = /[^\s]/; function tokenizeString(str) { var tokens = []; var position = 0; @@ -673,19 +674,20 @@ return (function () { return tokens; } - function isPossibleRelativeReference(token, last) { - return SYMBOL_START.exec(token.charAt(0)) && - token !== "true" && - token !== "false" && - token !== "this" && - last !== "."; - } + function isPossibleRelativeReference(token, last, paramName) { + return SYMBOL_START.exec(token.charAt(0)) && + token !== "true" && + token !== "false" && + token !== "this" && + token !== paramName && + last !== "."; + } - function maybeGenerateConditional(tokens) { + function maybeGenerateConditional(tokens, paramName) { if (tokens[0] === '[') { tokens.shift(); var bracketCount = 1; - var conditional = "(function(__val){ return ("; + var conditional = "(function(" + paramName + "){ return ("; var last = null; while (tokens.length > 0) { var token = tokens[0]; @@ -696,13 +698,18 @@ return (function () { conditional = conditional + "true"; } tokens.shift(); - return conditional + ")})"; + conditional += ")})"; + try { + return eval(conditional); + } catch (e) { + triggerErrorEvent(getDocument(), "htmx:syntax:error", {error:e}) + } } } else if (token === "[") { bracketCount++; } - if (isPossibleRelativeReference(token, last)) { - conditional = conditional += "((__val." + token + ") ? (__val." + token + ") : (" + token + "))"; + if (isPossibleRelativeReference(token, last, paramName)) { + conditional += "((" + paramName + "." + token + ") ? (" + paramName + "." + token + ") : (window." + token + "))"; } else { conditional = conditional + token; } @@ -711,49 +718,73 @@ return (function () { } } + function consumeUntil(tokens, match) { + var result = ""; + while (tokens.length > 0 && !tokens[0].match(match)) { + result += tokens.shift(); + } + return result; + } + function getTriggerSpecs(elt) { - var explicitTrigger = getAttributeValue(elt, 'hx-trigger'); + var triggerSpecs = []; if (explicitTrigger) { - var triggerSpecs = explicitTrigger.split(',').map(function(triggerString) { - var tokens = splitOnWhitespace(triggerString.trim()); - var trigger = tokens[0]; // splitOnWhitespace returns at least one element - if (!trigger) - return null; - - if (trigger === "every") - return {trigger: 'every', pollInterval: parseInterval(tokens[1])}; - if (trigger.indexOf("sse:") === 0) - return {trigger: 'sse', sseEvent: trigger.substr(4)}; - - var triggerSpec = {trigger: trigger}; - for (var i = 1; i < tokens.length; i++) { - var token = tokens[i].trim(); - if (token === "changed") { - triggerSpec.changed = true; - } - if (token === "once") { - triggerSpec.once = true; - } - if (token.indexOf("delay:") === 0) { - triggerSpec.delay = parseInterval(token.substr(6)); - } - if (token.indexOf("throttle:") === 0) { - triggerSpec.throttle = parseInterval(token.substr(9)); + var tokens = tokenizeString(explicitTrigger); + do { + consumeUntil(tokens, NOT_WHITESPACE); + var initialLength = tokens.length; + var trigger = consumeUntil(tokens, /[,\[\s]/); + if (trigger !== "") { + if (trigger === "every") { + var every = {trigger: 'every'}; + consumeUntil(tokens, NOT_WHITESPACE); + every.pollInterval = parseInterval(consumeUntil(tokens, WHITESPACE)); + triggerSpecs.push(every); + } else if (trigger.indexOf("sse:") === 0) { + triggerSpecs.push({trigger: 'sse', sseEvent: trigger.substr(4)}); + } else { + var triggerSpec = {trigger: trigger}; + var eventFilter = maybeGenerateConditional(tokens, "evt"); + if (eventFilter) { + triggerSpec.eventFilter = eventFilter; + } + while (tokens.length > 0 && tokens[0] !== ",") { + consumeUntil(tokens, NOT_WHITESPACE) + var token = tokens.shift(); + if (token === "changed") { + triggerSpec.changed = true; + } else if (token === "once") { + triggerSpec.once = true; + } else if (token === "delay" && tokens[0] === ":") { + tokens.shift(); + triggerSpec.delay = parseInterval(consumeUntil(tokens, WHITESPACE)); + } else if (token === "throttle" && tokens[0] === ":") { + tokens.shift(); + triggerSpec.throttle = parseInterval(consumeUntil(tokens, WHITESPACE)); + } else { + triggerErrorEvent(elt, "htmx:syntax:error", {token:tokens.shift()}); + } + } + triggerSpecs.push(triggerSpec); } } - return triggerSpec; - }).filter(function(x){ return x !== null }); - - if (triggerSpecs.length) - return triggerSpecs; + if (tokens.length === initialLength) { + triggerErrorEvent(elt, "htmx:syntax:error", {token:tokens.shift()}); + } + consumeUntil(tokens, NOT_WHITESPACE); + } while (tokens[0] === "," && tokens.shift()) } - if (matches(elt, 'form')) + if (triggerSpecs.length > 0) { + return triggerSpecs; + } else if (matches(elt, 'form')) { return [{trigger: 'submit'}]; - if (matches(elt, 'input, textarea, select')) + } else if (matches(elt, 'input, textarea, select')) { return [{trigger: 'change'}]; - return [{trigger: 'click'}]; + } else { + return [{trigger: 'click'}]; + } } function cancelPolling(elt) { @@ -806,6 +837,10 @@ return (function () { function addEventListener(elt, verb, path, nodeData, triggerSpec, explicitCancel) { var eventListener = function (evt) { + if (triggerSpec.eventFilter && + triggerSpec.eventFilter(evt) !== true) { + return; + } if (ignoreBoostedAnchorCtrlClick(elt, evt)) { return; } @@ -972,7 +1007,7 @@ return (function () { if (value[0] === "connect") { processSSESource(elt, value[1]); } - + if ((value[0] === "swap")) { processSSESwap(elt, value[1]) } @@ -1000,12 +1035,12 @@ return (function () { /////////////////////////// // TODO: merge this code with AJAX and WebSockets code in the future. - + var response = event.data; withExtensions(elt, function(extension){ response = extension.transformResponse(response, null, elt); }); - + var swapSpec = getSwapSpecification(elt) var target = getTarget(elt) var settleInfo = makeSettleInfo(elt); diff --git a/test/attributes/hx-trigger.js b/test/attributes/hx-trigger.js index 7c764947..8752f864 100644 --- a/test/attributes/hx-trigger.js +++ b/test/attributes/hx-trigger.js @@ -89,7 +89,6 @@ describe("hx-trigger attribute", function(){ it('non-default value works w/ data-* prefix', function() { this.server.respondWith("GET", "/test", "Clicked!"); - var form = make('
Click Me!
'); form.click(); form.innerHTML.should.equal("Click Me!"); @@ -113,8 +112,6 @@ describe("hx-trigger attribute", function(){ div.innerHTML.should.equal("Requests: 2"); }); - - it("parses spec strings", function() { var specExamples = { @@ -131,9 +128,7 @@ describe("hx-trigger attribute", function(){ "event1, event2": [{trigger: 'event1'}, {trigger: 'event2'}], "event1 once, event2 changed": [{trigger: 'event1', once: true}, {trigger: 'event2', changed: true}], "event1,": [{trigger: 'event1'}], - ",event1": [{trigger: 'event1'}], " ": [{trigger: 'click'}], - ",": [{trigger: 'click'}] } for (var specString in specExamples) { @@ -157,4 +152,19 @@ describe("hx-trigger attribute", function(){ spec.should.deep.equal([{trigger: 'change'}]); }) + it('filters properly with filter spec', function(){ + this.server.respondWith("GET", "/test", "Called!"); + var form = make('
Not Called
'); + form.click(); + form.innerHTML.should.equal("Not Called"); + var event = htmx._("makeEvent")('evt'); + form.dispatchEvent(event); + this.server.respond(); + form.innerHTML.should.equal("Not Called"); + event.foo = true; + form.dispatchEvent(event); + this.server.respond(); + form.innerHTML.should.equal("Called!"); + }) + }) diff --git a/test/core/tokenizer.js b/test/core/tokenizer.js index 5ca1e355..aad583a9 100644 --- a/test/core/tokenizer.js +++ b/test/core/tokenizer.js @@ -19,16 +19,16 @@ describe("Core htmx tokenizer tests", function(){ it('tokenizes properly', function() { tokenizeTest("", []); - tokenizeTest(" ", []); + tokenizeTest(" ", [" ", " "]); tokenizeTest("(", ["("]); tokenizeTest("()", ["(", ")"]); tokenizeTest("(,)", ["(", ",", ")"]); - tokenizeTest(" ( ) ", ["(", ")"]); - tokenizeTest(" && ) ", ["&&", ")"]); - tokenizeTest(" && ) 'asdf'", ["&&", ")", "'asdf'"]); - tokenizeTest(" && ) ',asdf'", ["&&", ")", "',asdf'"]); + tokenizeTest(" ( ) ", [" ", "(", " ", ")", " "]); + tokenizeTest(" && ) ", [" ", "&", "&", " ", ")", " "]); + tokenizeTest(" && ) 'asdf'", [" ", "&", "&", " ", ")", " ", "'asdf'"]); + tokenizeTest(" && ) ',asdf'", [" ", "&", "&", " ", ")", " ", "',asdf'"]); tokenizeTest('",asdf"', ['",asdf"']); - tokenizeTest('&& ) ",asdf"', ["&&", ")", '",asdf"']); + tokenizeTest('&& ) ",asdf"', ["&", "&", " ", ")", " ", '",asdf"']); }); it('generates conditionals property', function()