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
#### Alterations
* 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.
* `(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.)
......
......@@ -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.
*/
return "blocked";
return { blocked: true };
},
[String])
......@@ -676,9 +676,9 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
*/
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',
{name:"No", cancel:true, callback: () => section.unblock()},
],
});
return "blocked";
return { blocked: d };
}
requestAnimationFrame(Engine.showPassage.bind(Engine, State.passage, false /* stretchtext value */));
},
......@@ -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.
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 shouldn't fire.
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 (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.
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.
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:
(cycling-link:), (prompt:), (confirm:)
......@@ -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.
*/
dialog({
const d = dialog({
section, message, cd,
buttons: buttons.map(b => ({
name: b,
......@@ -2600,8 +2600,8 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
section.unblock((varBind && varBind.set(b)) || '');
},
})),
});
return "blocked";
})
; return { blocked: d };
},
[either(VarBind, String), zeroOrMore(String)])
......@@ -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.
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.
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',
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>.
setTimeout(() => d.find('input').last().focus(), 100);
},
......@@ -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.
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.
See also:
......@@ -3082,7 +3089,7 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
if (confirmButton === "") {
return TwineError.create("datatype", "The text for (confirm:)'s confirm link can't be blank.");
}
dialog({
const d = dialog({
section, message,
defaultValue: false,
buttons:[{
......@@ -3095,6 +3102,11 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
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)])
......
......@@ -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:)
has the same semantics as "return" in other programming languages.
*/
|| "blocked";
|| { blocked: true };
}, [Any],
/*attachable:false*/ false);
......@@ -372,7 +372,7 @@ define(['utils', 'macros', 'state', 'utils/operationutils', 'datatypes/changerco
}
},
({stack}, message) => {
return outputValue("error", stack, TwineError.create("user", message)) || "blocked";
return outputValue("error", stack, TwineError.create("user", message)) || { blocked: true };
},
[String],
/*attachable:false*/false);
......
......@@ -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
(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) {
return;
......
......@@ -49,7 +49,13 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'engine', 'datatypes/c
(which means the click input should be dropped).
*/
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;
}
......
......@@ -15,7 +15,12 @@ define(['macros', 'utils', 'utils/operationutils', 'datatypes/colour', 'datatype
String data
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
the passage source. If a string contains markup, then the markup will be processed when it's
......
......@@ -17,7 +17,7 @@ define([
'internaltypes/twineerror',
'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;
......@@ -388,12 +388,12 @@ define([
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:),
or interruption macros like (goto:).
*/
else if (result === "blocked") {
this.stackTop.blocked = true;
else if (isObject(result) && result.blocked) {
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)
that blocked the section, and put them in the original initiating <tw-expression>.
......@@ -592,7 +592,7 @@ define([
- collapses: Boolean (used by collapsing markup)
- lastHookShown: Boolean (used by (else:) and (elseif:))
- 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)
- evaluateOnly: String (used by evaluateTwineMarkup())
- finalIter: Boolean (used for infinite loop checks)
......@@ -1028,6 +1028,9 @@ define([
/*
The first blocker can now be taken out and run, which
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;
let error = this.eval(blockers.shift());
......
......@@ -299,6 +299,35 @@ describe("basic command macros", function() {
},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) {
var p = runPassage("foo(mouseover:'foo')[bar](dialog:'baz')");
expect(p.text()).toBe("foo");
......@@ -397,6 +426,15 @@ describe("basic command macros", function() {
},60);
},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() {
var p = runPassage("|a>[foo]("+name+":)(replace:?a)[bar]");
expect(p.find('tw-hook').text()).toBe("foo");
......
......@@ -233,7 +233,7 @@ describe("patterns", function() {
expect("(set: $a to '2' is a anycase)(print:$a)").markupToPrint("false");
});
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 '\\n' 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