Commit 23631f8e authored by Leon Arnott's avatar Leon Arnott
Browse files

Began implementing (dropdown:), a dropdown <select> menu HookCommand that alters a bound variable.

I still need to add some more tests, in particular regarding its interaction with various changers.

Also:

 * Fixed a bug in which "when" lambdas weren't firing correctly.

 * Fixed a bug where clicking "undo" and "redo" icons would pass a jQuery click event to goBack() or goForward().
parent b22700969a1c
......@@ -35,6 +35,17 @@ define(['utils', 'utils/operationutils', 'internaltypes/varref', 'internaltypes/
TwineScript_ObjectName: "a bound variable",
TwineScript_Unstorable: true,
/*
Setting a value in a VarBind is fairly straightforward - simply set the varRef, and then pass up any errors.
*/
set(value) {
const result = this.varRef.set(value);
let error;
if ((error = TwineError.containsError(result))) {
return error;
}
},
/*
bind is either "one way" (the DOM element's first provided value is automatically selected and
......
......@@ -45,8 +45,8 @@ define(['jquery', 'utils', 'utils/selectors', 'state', 'section', 'passages'],
}
// Apart from the Permalink, the sidebar buttons consist of Undo (Back) and Redo (Forward) buttons.
const
back = $('<tw-icon tabindex=0 class="undo" title="Undo">&#8630;</tw-icon>').click(Engine.goBack),
fwd = $('<tw-icon tabindex=0 class="redo" title="Redo">&#8631;</tw-icon>').click(Engine.goForward);
back = $('<tw-icon tabindex=0 class="undo" title="Undo">&#8630;</tw-icon>').click(() => Engine.goBack()),
fwd = $('<tw-icon tabindex=0 class="redo" title="Redo">&#8631;</tw-icon>').click(() => Engine.goForward());
if (State.pastLength <= 0) {
back.css("visibility", "hidden");
......
"use strict";
define(['requestAnimationFrame', 'macros', 'utils', 'state', 'passages', 'engine', 'internaltypes/twineerror', 'datatypes/hookset', 'datatypes/varbind', 'utils/operationutils'],
(requestAnimationFrame, Macros, {toJSLiteral, unescape}, State, Passages, Engine, TwineError, HookSet, VarBind, {printBuiltinValue}) => {
define(['jquery', 'requestAnimationFrame', 'macros', 'utils', 'utils/selectors', 'state', 'passages', 'engine', 'internaltypes/twineerror', 'datatypes/hookset', 'datatypes/varbind', 'utils/operationutils'],
($, requestAnimationFrame, Macros, {storyElement, toJSLiteral, unescape}, Selectors, State, Passages, Engine, TwineError, HookSet, VarBind, {printBuiltinValue}) => {
/*d:
Command data
......@@ -341,17 +341,6 @@ define(['requestAnimationFrame', 'macros', 'utils', 'state', 'passages', 'engine
bind = labels.shift();
}
let index = 0;
/*
An operation that should be performed when this is initially printed, and
every time it is clicked: set the bound variable to match the current label.
*/
const setBoundVariable = (labelValue) => {
const result = bind.varRef.set(labelValue);
let error;
if ((error = TwineError.containsError(result))) {
return error;
}
};
cd.data.clickEvent = (link) => {
/*
......@@ -364,7 +353,7 @@ define(['requestAnimationFrame', 'macros', 'utils', 'state', 'passages', 'engine
(If it returns an error, let that replace the source.)
*/
if (bind) {
const result = setBoundVariable(labels[index]);
const result = bind.set(labels[index]);
if (TwineError.containsError(result)) {
/*
As this clickEvent occurs when the interface element has already been rendered,
......@@ -392,16 +381,74 @@ define(['requestAnimationFrame', 'macros', 'utils', 'state', 'passages', 'engine
*/
let source = '<tw-link>' + labels[0] + '</tw-link>';
if (bind) {
const result = setBoundVariable(labels[index]);
const result = bind.set(labels[index]);
if (TwineError.containsError(result)) {
return result;
}
}
assign(cd, { source, append: "replace", });
return cd;
return assign(cd, { source, append: "replace", });
},
[either(VarBind, String), rest(String)]);
/*
An onchange event for <select> elements must be registered for the sake of the (dropdown:) macro,
which is implemented similar to the link macros - the ChangeDescriptor's data.dropdownEvent indicates
what to do when the <select> is interacted with.
*/
$(() => $(storyElement).on(
/*
The jQuery event namespace is "dropdown-macro".
*/
"change.dropdown-macro",
"select",
function changeDropdownEvent() {
const dropdown = $(this),
/*
Dropdowns' events are, due to limitations in the ChangeDescriptor format,
attached to the <tw-hook> or <tw-expression> containing the element.
*/
event = dropdown.closest('tw-expression, tw-hook').data('dropdownEvent');
if (event) {
event(dropdown);
}
}
));
/*d:
(dropdown: Bind, ...String) -> HookCommand
TBW
*/
Macros.addHookCommand("dropdown",
() => {},
(cd, _, bind, ...labels) => {
let source = '<select>'
+ labels.map((label, i) =>
'<option' + (i === 0 ? ' selected' : '') + '>' + label + '</option>'
).join('\n')
+ '</select>';
cd.data.dropdownEvent = (dropdownMenu) => {
const value = dropdownMenu.val();
const result = bind.set(value);
if (TwineError.containsError(result)) {
/*
As this clickEvent occurs when the interface element has already been rendered,
we need to explicitly replace the link with the rendered error rather than return
the error (to nobody).
*/
dropdownMenu.replaceWith(result.render(value));
return;
}
};
const result = bind.set(labels[0]);
if (TwineError.containsError(result)) {
return result;
}
return assign(cd, { source, append: "replace", });
},
[VarBind, String, rest(String)]);
Macros.addCommand
/*d:
(show: ...HookName) -> Command
......
......@@ -556,7 +556,7 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'engine',
See also:
(link:), (link-reveal:), (link-repeat:), (click:), (mouseout:), (replace:), (mouseover-replace:), (hover-style:)
#links 11
#links 12
*/
{
name: "mouseover",
......@@ -594,7 +594,7 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'engine',
See also:
(link:), (link-reveal:), (link-repeat:), (click:), (mouseover:), (replace:), (mouseout-replace:), (hover-style:)
#links 15
#links 17
*/
{
name: "mouseout",
......@@ -673,7 +673,7 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'engine',
This is similar to (click-replace:), but uses the (mouseover:) macro's behaviour instead of
(click:)'s. For more information, consult the description of (click-replace:).
#links 12
#links 13
*/
/*d:
(mouseover-append: HookName or String) -> Changer
......@@ -681,7 +681,7 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'engine',
This is similar to (click-append:), but uses the (mouseover:) macro's behaviour instead of
(click:)'s. For more information, consult the description of (click-append:).
#links 13
#links 14
*/
/*d:
(mouseover-prepend: HookName or String) -> Changer
......@@ -689,7 +689,7 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'engine',
This is similar to (click-prepend:), but uses the (mouseover:) macro's behaviour instead of
(click:)'s. For more information, consult the description of (click-prepend:).
#links 14
#links 15
*/
/*d:
(mouseout-replace: HookName or String) -> Changer
......@@ -697,7 +697,7 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'engine',
This is similar to (click-replace:), but uses the (mouseout:) macro's behaviour instead of
(click:)'s. For more information, consult the description of (click-replace:).
#links 16
#links 18
*/
/*d:
(mouseout-append: HookName or String) -> Changer
......@@ -705,7 +705,7 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'engine',
This is similar to (click-append:), but uses the (mouseout:) macro's behaviour instead of
(click:)'s. For more information, consult the description of (click-append:).
#links 17
#links 19
*/
/*d:
(mouseout-prepend: HookName or String) -> Changer
......@@ -713,7 +713,7 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'engine',
This is similar to (click-prepend:), but uses the (mouseout:) macro's behaviour instead of
(click:)'s. For more information, consult the description of (click-prepend:).
#links 18
#links 20
*/
revisionTypes.forEach((revisionType) => {
interactionTypes.forEach((interactionType) => {
......@@ -739,7 +739,7 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'engine',
See also:
(link-goto:)
#links 19
#links 11
*/
/*d:
(mouseover-goto: HookName or String, String) -> HookCommand
......@@ -747,7 +747,7 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'engine',
This is similar to (click-goto:), but uses the (mouseover:) macro's behaviour instead of
(click:)'s. For more information, consult the description of (click-goto:).
#links 20
#links 16
*/
/*d:
(mouseout-goto: HookName or String, String) -> HookCommand
......
......@@ -606,7 +606,7 @@ define([
*/
function runLiveHook(expr, target, delay = 20, event = undefined) {
if (event) {
Utils.assertMustHave(event, "when");
Utils.assertMustHave(event, ["when"]);
}
/*
Obtain the code of the hook that the (live:) or (event:) changer suppressed.
......@@ -628,13 +628,17 @@ define([
/*
If this is an (event:) command, check the event (which should be a "when" lambda)
and if it's not happened yet, wait for the next timeout.
Note: Lambda.filter() returns the passed-in array with values filtered out based on
whether the lambda was false. So, passing in 'true' will return [true] if
the lambda was true and [] (an empty array) if not.
*/
const eventFired = (event && event.filter(this, [undefined]));
const eventFired = (event && event.filter(this, [true]));
if (TwineError.containsError(eventFired)) {
eventFired.render(expr.attr('title')).replaceAll(expr);
return;
}
if (event && !eventFired) {
if (event && !eventFired[0]) {
setTimeout(recursive, delay);
return;
}
......
......@@ -75,4 +75,4 @@ tw-broken-link {
&:active {
color: hsla(7, 71%, 55%, 0.5); // $activecolour at 0.5 opacity
}
}
\ No newline at end of file
}
......@@ -183,3 +183,18 @@ tw-colour {
width: 1em;
height: 1em;
}
/*
These are created by (dropdown:).
This selector should be more specific, but I'm concerned about it being harder for user stylesheets
to override... so I'm leaving it as-is.
*/
select {
&:not([disabled]) {
color: inherit;
}
background-color: inherit;
font:inherit;
border-style:solid;
padding: 2px;
}
......@@ -92,8 +92,8 @@ describe("link macros", function() {
var p = runPassage("(link-repeat:'A')[B(set:$c to 12)]");
p.find('tw-link').click();
expect(p.text()).toBe("AB");
expect("$c").markupToPrint("12");
expect(p.find('tw-link').length).toBe(1);
expect("$c").markupToPrint("12");
});
it("the link can be clicked multiple times", function() {
var p = runPassage("(set:$c to 0)(link-repeat:'A')[B(set:$c to it + 12)]");
......
......@@ -73,6 +73,12 @@ describe("interface macros", function(){
p.find('tw-link').click();
expect("$foo").markupToPrint('qux');
});
xit("works with temp variables", function() {
var p = runPassage("(cycling-link: bind _foo, 'bar', 'baz', 'qux')(event: _foo is 'qux')[quux]");
p.find('tw-link').click();
p.find('tw-link').click();
expect(p.text()).toBe("quxquux");
});
it("errors if the bind is invalid", function() {
expect("(set:$foo to 1)(cycling-link: bind $foo's 1st, 'bar','baz', 'qux')").markupToError();
});
......@@ -96,4 +102,34 @@ describe("interface macros", function(){
});
});
});
describe("the (dropdown:) macro", function() {
it("accepts one bound variable, and two or more strings", function() {
expect("(print:(dropdown:))").markupToError();
expect("(print:(dropdown:''))").markupToError();
expect("(print:(dropdown:'baz'))").markupToError();
expect("(print:(dropdown:2))").markupToError();
expect("(print:(dropdown:false))").markupToError();
expect("(print:(dropdown:bind $foo))").markupToError();
expect("(print:(dropdown:'baz', 'baz'))").markupToError();
expect("(print:(dropdown:bind $foo, 'baz'))").markupToError();
expect("(print:(dropdown:bind $foo, 'baz', 'qux'))").not.markupToError();
});
it("creates a <select> element with each string as an <option>", function() {
var p = runPassage("(dropdown: bind $foo, 'bar','baz','qux')(event: when $foo is 'qux')[quux]");
expect(p.find('select').length).toBe(1);
expect(p.find('select option').length).toBe(3);
});
it("when changed, sets the variable to the string label", function() {
var p = runPassage("(dropdown: bind $foo, 'bar','baz', 'qux')");
expect("$foo").markupToPrint('bar');
p = runPassage("(dropdown: bind $foo, 'bar','baz', 'qux')");
p.find('select').val('baz').change();
expect("$foo").markupToPrint('baz');
p = runPassage("(dropdown: bind $foo, 'bar','baz', 'qux')");
p.find('select').val('qux').change();
expect("$foo").markupToPrint('qux');
});
});
});
......@@ -330,10 +330,10 @@ afterAll(function() {
<script src="spec/changer_macros/hooks.js"></script>
<script src="spec/changer_macros/enchantment.js"></script>
<script src="spec/changer_macros/interaction.js"></script>
<script src="spec/changer_macros/interface.js"></script>
<script src="spec/changer_macros/revision.js"></script>
<script src="spec/command_macros/basics.js"></script>
<script src="spec/command_macros/interface.js"></script>
<script src="spec/command_macros/live.js"></script>
<script src="spec/command_macros/saving.js"></script>
......
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