Read about our upcoming Code of Conduct on this issue

Commit ae6b035b authored by Leon Arnott's avatar Leon Arnott
Browse files

Links (created by (link:) and related macros) now work inside dialogs' body prose.

Note that enchantments and interaction macros such as (click:) continue to not affect prose inside dialog boxes.
parent beb7e64b1c47
...@@ -20,6 +20,7 @@ Documentation is at http://twine2.neocities.org/. See below for compilation inst ...@@ -20,6 +20,7 @@ Documentation is at http://twine2.neocities.org/. See below for compilation inst
#### Alterations #### Alterations
* Certain kinds of intense substring or subarray data names, such as `$a's (range:1,65535)`, have had their runtime performance improved. * Certain kinds of intense substring or subarray data names, such as `$a's (range:1,65535)`, have had their runtime performance improved.
* Links (created by `(link:)` and related macros) now work inside `(dialog:)`, `(prompt:)` and `(confirm:)`'s body prose. Note that enchantments and interaction macros such as `(click:)` continue to not affect prose inside dialog boxes.
* Made the syntax colour of the lambda keywords (`where`, `when`, `via`, `each` and `making`) slightly brighter. * Made the syntax colour of the lambda keywords (`where`, `when`, `via`, `each` and `making`) slightly brighter.
* `(mock-visits:)` effects are now saved using `(save-game:)` in Debug Mode. * `(mock-visits:)` effects are now saved using `(save-game:)` in Debug Mode.
* Added `newline` as an alias for the `linebreak` datatype. (Some parts of the documentation previously erroneously used the former instead of the latter.) * Added `newline` as an alias for the `linebreak` datatype. (Some parts of the documentation previously erroneously used the former instead of the latter.)
......
...@@ -624,7 +624,7 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine', ...@@ -624,7 +624,7 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
/* /*
Blocking the passage immediately ceases rendering; note that this should never get unblocked. Blocking the passage immediately ceases rendering; note that this should never get unblocked.
*/ */
return "blocked"; return { blocked: true };
}, },
[String]) [String])
...@@ -676,9 +676,9 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine', ...@@ -676,9 +676,9 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
*/ */
requestAnimationFrame(()=> Engine.goBack({ transition: cd.data.passageT8n })); requestAnimationFrame(()=> Engine.goBack({ transition: cd.data.passageT8n }));
/* /*
As with the (goto:) macro, "blocked" signals to Section's runExpression() to cease evaluation. As with the (goto:) macro, {blocked} signals to Section's runExpression() to cease evaluation.
*/ */
return "blocked"; return { blocked: true };
}, },
[]); []);
...@@ -2436,7 +2436,7 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine', ...@@ -2436,7 +2436,7 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
{name:"No", cancel:true, callback: () => section.unblock()}, {name:"No", cancel:true, callback: () => section.unblock()},
], ],
}); });
return "blocked"; return { blocked: d };
} }
requestAnimationFrame(Engine.showPassage.bind(Engine, State.passage, false /* stretchtext value */)); requestAnimationFrame(Engine.showPassage.bind(Engine, State.passage, false /* stretchtext value */));
}, },
...@@ -2529,16 +2529,16 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine', ...@@ -2529,16 +2529,16 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
If no button strings are given, a single link reading "OK" will be used. Giving empty strings for any of the links will cause an error. If no button strings are given, a single link reading "OK" will be used. Giving empty strings for any of the links will cause an error.
When the dialog is on-screen, the entire game is essentially "paused" - until it is dismissed, no further computations are performed, links can't be clicked, When the dialog is on-screen, the entire game is essentially "paused" - until it is dismissed,
and (live:) and (event:) macros shouldn't fire. no further computations are performed, links can't be clicked (except links inside the dialog text itself),
(click:), (mouseover:) and (mouseout:) enchantments shouldn't work, and (live:) and (event:) macros
shouldn't fire.
For obvious reasons, you cannot supply a two-way bound variable to this macro. Doing so will cause an error to result. For obvious reasons, you cannot supply a two-way bound variable to this macro. Doing so will cause an error to result.
From version 3.2.0 on, it is possible to attach changers to this command. `(t8n:'slide-up')+(text-rotate-x:25)(dialog:"EMAIL SENT!")`, for instance, produces a dialog From version 3.2.0 on, it is possible to attach changers to this command. `(t8n:'slide-up')+(text-rotate-x:25)(dialog:"EMAIL SENT!")`, for instance, produces a dialog
that's tilted upward, and which slides upward when it appears. that's tilted upward, and which slides upward when it appears.
You can customise the dialog by attaching changers to this command. Attaching a (transition:) command will cause the dialog to appear using that transition.
See also: See also:
(cycling-link:), (prompt:), (confirm:) (cycling-link:), (prompt:), (confirm:)
...@@ -2587,7 +2587,7 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine', ...@@ -2587,7 +2587,7 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
/* /*
Create the dialog, passing in the ChangeDescriptor, to be used when rendering the string. Create the dialog, passing in the ChangeDescriptor, to be used when rendering the string.
*/ */
dialog({ const d = dialog({
section, message, cd, section, message, cd,
buttons: buttons.map(b => ({ buttons: buttons.map(b => ({
name: b, name: b,
...@@ -2600,8 +2600,8 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine', ...@@ -2600,8 +2600,8 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
section.unblock((varBind && varBind.set(b)) || ''); section.unblock((varBind && varBind.set(b)) || '');
}, },
})), })),
}); })
return "blocked"; ; return { blocked: d };
}, },
[either(VarBind, String), zeroOrMore(String)]) [either(VarBind, String), zeroOrMore(String)])
...@@ -2996,7 +2996,8 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine', ...@@ -2996,7 +2996,8 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
support certain newer browsers that no longer offer this function, the macro was changed. support certain newer browsers that no longer offer this function, the macro was changed.
When the dialog is on-screen, the entire game is essentially "paused" - until it is dismissed, When the dialog is on-screen, the entire game is essentially "paused" - until it is dismissed,
no further computations are performed, links can't be clicked, and (live:) and (event:) macros no further computations are performed, links can't be clicked (except links inside the dialog text itself),
(click:), (mouseover:) and (mouseout:) enchantments shouldn't work, and (live:) and (event:) macros
shouldn't fire. shouldn't fire.
A note about player-submitted strings: because most string-printing functionality in Harlowe (the (print:) macro, A note about player-submitted strings: because most string-printing functionality in Harlowe (the (print:) macro,
...@@ -3036,6 +3037,11 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine', ...@@ -3036,6 +3037,11 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
callback: () => section.unblock(defaultValue), callback: () => section.unblock(defaultValue),
}), }),
}); });
/*
The blocking dialog is passed to section.stackTop.blocked. This allows
certain local events within the dialog (such as (link:)s) to still be usable.
*/
section.stackTop.blocked = d;
// Regrettably, this arbitrary timeout seems to be the only reliable way to focus the <input>. // Regrettably, this arbitrary timeout seems to be the only reliable way to focus the <input>.
setTimeout(() => d.find('input').last().focus(), 100); setTimeout(() => d.find('input').last().focus(), 100);
}, },
...@@ -3065,7 +3071,8 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine', ...@@ -3065,7 +3071,8 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
support certain newer browsers that no longer offer this function, the macro was changed. support certain newer browsers that no longer offer this function, the macro was changed.
When the dialog is on-screen, the entire game is essentially "paused" - until it is dismissed, When the dialog is on-screen, the entire game is essentially "paused" - until it is dismissed,
no further computations are performed, links can't be clicked, and (live:) and (event:) macros no further computations are performed, links can't be clicked (except links inside the dialog text itself),
(click:), (mouseover:) and (mouseout:) enchantments shouldn't work, and (live:) and (event:) macros
shouldn't fire. shouldn't fire.
See also: See also:
...@@ -3082,7 +3089,7 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine', ...@@ -3082,7 +3089,7 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
if (confirmButton === "") { if (confirmButton === "") {
return TwineError.create("datatype", "The text for (confirm:)'s confirm link can't be blank."); return TwineError.create("datatype", "The text for (confirm:)'s confirm link can't be blank.");
} }
dialog({ const d = dialog({
section, message, section, message,
defaultValue: false, defaultValue: false,
buttons:[{ buttons:[{
...@@ -3095,6 +3102,11 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine', ...@@ -3095,6 +3102,11 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
callback: () => section.unblock(false), callback: () => section.unblock(false),
}), }),
}); });
/*
The blocking dialog is passed to section.stackTop.blocked. This allows
certain local events within the dialog (such as (link:)s) to still be usable.
*/
section.stackTop.blocked = d;
}, },
[String, optional(String), optional(String)]) [String, optional(String), optional(String)])
......
...@@ -237,7 +237,7 @@ define(['utils', 'macros', 'state', 'utils/operationutils', 'datatypes/changerco ...@@ -237,7 +237,7 @@ define(['utils', 'macros', 'state', 'utils/operationutils', 'datatypes/changerco
By forcibly blocking the control flow of the section after executing this macro, (output:) By forcibly blocking the control flow of the section after executing this macro, (output:)
has the same semantics as "return" in other programming languages. has the same semantics as "return" in other programming languages.
*/ */
|| "blocked"; || { blocked: true };
}, [Any], }, [Any],
/*attachable:false*/ false); /*attachable:false*/ false);
...@@ -372,7 +372,7 @@ define(['utils', 'macros', 'state', 'utils/operationutils', 'datatypes/changerco ...@@ -372,7 +372,7 @@ define(['utils', 'macros', 'state', 'utils/operationutils', 'datatypes/changerco
} }
}, },
({stack}, message) => { ({stack}, message) => {
return outputValue("error", stack, TwineError.create("user", message)) || "blocked"; return outputValue("error", stack, TwineError.create("user", message)) || { blocked: true };
}, },
[String], [String],
/*attachable:false*/false); /*attachable:false*/false);
......
...@@ -886,6 +886,9 @@ define(['jquery', 'utils', 'utils/operationutils', 'engine', 'state', 'passages' ...@@ -886,6 +886,9 @@ define(['jquery', 'utils', 'utils/operationutils', 'engine', 'state', 'passages'
/* /*
First, don't do anything if control flow in the section is currently blocked First, don't do anything if control flow in the section is currently blocked
(which means the click/mouseover input should be dropped). (which means the click/mouseover input should be dropped).
Note that this currently (October 2021) does not allow enchantment events to fire
even within a (dialog:) or other element which blocks control flow.
*/ */
if (desc.section.stackTop && desc.section.stackTop.blocked) { if (desc.section.stackTop && desc.section.stackTop.blocked) {
return; return;
......
...@@ -49,7 +49,13 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'engine', 'datatypes/c ...@@ -49,7 +49,13 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'engine', 'datatypes/c
(which means the click input should be dropped). (which means the click input should be dropped).
*/ */
section = closest.data('section'); section = closest.data('section');
if (section && section.stackTop && section.stackTop.blocked) { if (section && section.stackTop && section.stackTop.blocked &&
/*
However, links inside (dialog:)s and other blocking elements may still have their
events occur. This is currently (as of October 2021) distinct from (click:) enchantments, which
are "passage-wide" and thus remain blocked.
*/
(!(section.stackTop.blocked instanceof $) || !section.stackTop.blocked.find(link).length)) {
return; return;
} }
......
...@@ -15,7 +15,12 @@ define(['macros', 'utils', 'utils/operationutils', 'datatypes/colour', 'datatype ...@@ -15,7 +15,12 @@ define(['macros', 'utils', 'utils/operationutils', 'datatypes/colour', 'datatype
String data String data
A string is just a run of text - a sequence of text characters strung together. To indicate that a A string is just a run of text - a sequence of text characters strung together. To indicate that a
sequence of characters is a string, place a matching pair of either " or ' characters around them. sequence of characters is a string, place a matching pair of either `"` or `'` characters around them. If you want
to include a `"` or `'` inside a string that is enclosed with a pair of that character, you can use the `\` symbol to
escape that character. `\"` and `\"` will become a `"` and `'`, respectively.
If you want to include a `\` character by itself, write `\\`.
You can place line breaks inside strings, thus causing the strings to span multiple lines.
When making a story, you'll mostly work with strings that you intend to insert into When making a story, you'll mostly work with strings that you intend to insert into
the passage source. If a string contains markup, then the markup will be processed when it's the passage source. If a string contains markup, then the markup will be processed when it's
......
...@@ -17,7 +17,7 @@ define([ ...@@ -17,7 +17,7 @@ define([
'internaltypes/twineerror', 'internaltypes/twineerror',
'internaltypes/twinenotifier', 'internaltypes/twinenotifier',
], ],
($, Utils, Renderer, Environ, Operations, State, {printBuiltinValue,objectName,typeID}, {collapse}, ChangerCommand, HookSet, Colour, Lambda, ChangeDescriptor, VarScope, TwineError, TwineNotifier) => { ($, Utils, Renderer, Environ, Operations, State, {printBuiltinValue,objectName,typeID,isObject}, {collapse}, ChangerCommand, HookSet, Colour, Lambda, ChangeDescriptor, VarScope, TwineError, TwineNotifier) => {
let Section; let Section;
...@@ -388,12 +388,12 @@ define([ ...@@ -388,12 +388,12 @@ define([
this.renderInto('', nextElem, result); this.renderInto('', nextElem, result);
} }
/* /*
If TwineScript_Run returns the string "blocked", If TwineScript_Run returns a {blocked} object,
then block control flow. This is usually caused by dialog macros like (alert:) or (confirm:), then block control flow. This is usually caused by dialog macros like (alert:) or (confirm:),
or interruption macros like (goto:). or interruption macros like (goto:).
*/ */
else if (result === "blocked") { else if (isObject(result) && result.blocked) {
this.stackTop.blocked = true; this.stackTop.blocked = result.blocked;
/* /*
This #awkward reinjection of JS blocker code is the only way to get errors back from a command (i.e. not a value macro) This #awkward reinjection of JS blocker code is the only way to get errors back from a command (i.e. not a value macro)
that blocked the section, and put them in the original initiating <tw-expression>. that blocked the section, and put them in the original initiating <tw-expression>.
...@@ -592,7 +592,7 @@ define([ ...@@ -592,7 +592,7 @@ define([
- collapses: Boolean (used by collapsing markup) - collapses: Boolean (used by collapsing markup)
- lastHookShown: Boolean (used by (else:) and (elseif:)) - lastHookShown: Boolean (used by (else:) and (elseif:))
- dom: jQuery (used by blockers) - dom: jQuery (used by blockers)
- blocked: Boolean (used by blockers) - blocked: Boolean or jQuery (used by blockers; a jQuery denotes the blocking element)
- blockedValues: Array (used by blockers) - blockedValues: Array (used by blockers)
- evaluateOnly: String (used by evaluateTwineMarkup()) - evaluateOnly: String (used by evaluateTwineMarkup())
- finalIter: Boolean (used for infinite loop checks) - finalIter: Boolean (used for infinite loop checks)
...@@ -1028,6 +1028,9 @@ define([ ...@@ -1028,6 +1028,9 @@ define([
/* /*
The first blocker can now be taken out and run, which The first blocker can now be taken out and run, which
blocks this section and ends execution. blocks this section and ends execution.
The blocker's own macro call may cause this.stackTop to become a jQuery of the blocked
element, so that events can still fire within it. Thus, this setting is only a special backup.
*/ */
this.stackTop.blocked = true; this.stackTop.blocked = true;
let error = this.eval(blockers.shift()); let error = this.eval(blockers.shift());
......
...@@ -299,6 +299,35 @@ describe("basic command macros", function() { ...@@ -299,6 +299,35 @@ describe("basic command macros", function() {
},20); },20);
},20); },20);
}); });
it("does not block link interaction inside the dialog itself", function(done) {
var p = runPassage("(dialog:'(color:green)[baz(link:\"qux\")[foo]]')");
expect($("tw-dialog > tw-hook").text()).toBe("bazqux");
$("tw-dialog").find('tw-link').first().click();
setTimeout(function() {
expect($("tw-dialog > tw-hook").text()).toBe("bazfoo");
done();
},20);
});
/* Dialogs need to be reimplemented as separate sections for these two to be usable */
xit("does not block (click:) interaction inside the dialog itself", function(done) {
p = runPassage("(dialog:'|A>[baz(click:?A)[foo]]')");
expect($("tw-dialog > tw-hook").text()).toBe("baz");
$('tw-dialog > tw-enchantment').click();
setTimeout(function() {
expect(p.text()).toBe("bazfoo");
done();
},20);
});
xit("does not block enchantments", function(done) {
var p = runPassage("(dialog:'|A>[baz](enchant:?A,(text-colour:#440044))')");
expect($("tw-dialog > tw-hook").css('color')).toMatch(/rgba?\(68,\s?0,\s?68[\),]/);
$("tw-dialog").find('tw-link').first().click();
setTimeout(function() {
p = runPassage("(enchant:?A,(text-colour:#440044))(dialog:'|A>[qux]')");
expect($("tw-dialog > tw-hook").css('color')).toMatch(/rgba?\(68,\s?0,\s?68[\),]/);
done();
},20);
});
it("blocks mouseover interaction when the dialog is present", function(done) { it("blocks mouseover interaction when the dialog is present", function(done) {
var p = runPassage("foo(mouseover:'foo')[bar](dialog:'baz')"); var p = runPassage("foo(mouseover:'foo')[bar](dialog:'baz')");
expect(p.text()).toBe("foo"); expect(p.text()).toBe("foo");
...@@ -397,6 +426,15 @@ describe("basic command macros", function() { ...@@ -397,6 +426,15 @@ describe("basic command macros", function() {
},60); },60);
},20); },20);
}); });
it("does not block link interaction inside the dialog itself", function(done) {
var p = runPassage("("+name+":'(color:green)[baz(link:\"qux\")[foo]]'" + (confirm ? "" : ",'foo'") + ")");
expect($("tw-dialog > tw-hook").text()).toBe("bazqux");
$("tw-dialog").find('tw-link').first().click();
setTimeout(function() {
expect($("tw-dialog > tw-hook").text()).toBe("bazfoo");
done();
},20);
});
it("doesn't block control flow if it errors", function() { it("doesn't block control flow if it errors", function() {
var p = runPassage("|a>[foo]("+name+":)(replace:?a)[bar]"); var p = runPassage("|a>[foo]("+name+":)(replace:?a)[bar]");
expect(p.find('tw-hook').text()).toBe("foo"); expect(p.find('tw-hook').text()).toBe("foo");
......
...@@ -233,7 +233,7 @@ describe("patterns", function() { ...@@ -233,7 +233,7 @@ describe("patterns", function() {
expect("(set: $a to '2' is a anycase)(print:$a)").markupToPrint("false"); expect("(set: $a to '2' is a anycase)(print:$a)").markupToPrint("false");
}); });
it("'linebreak' or 'newline' matches line breaks", function() { it("'linebreak' or 'newline' matches line breaks", function() {
['linebreak','newline'].forEach(e=>{ ['linebreak','newline'].forEach(function(e){
expect("(set: $a to '\\r\\n' is a "+e+")(print:$a)").markupToPrint("true"); expect("(set: $a to '\\r\\n' is a "+e+")(print:$a)").markupToPrint("true");
expect("(set: $a to '\\n' is a "+e+")(print:$a)").markupToPrint("true"); expect("(set: $a to '\\n' is a "+e+")(print:$a)").markupToPrint("true");
expect("(set: $a to '\\r' is a "+e+")(print:$a)").markupToPrint("true"); expect("(set: $a to '\\r' is a "+e+")(print:$a)").markupToPrint("true");
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment