Read about our upcoming Code of Conduct on this issue

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

Added (button:), a styling changer for links, as well as added an optional...

Added (button:), a styling changer for links, as well as added an optional "unavailable" text string argument to (link-storylet:).

Having a drop-in styling option for links that makes them more closely resemble other contemporary IF engines' links has been something I've gradually accepted as desirable. That being said, getting this working with every kind of "link" in this engine has forced me to use a CSS class name (a namespace I'm loathe to use unnecessarily).

I feel a bit more positive about (link-storylet:) now that the optional argument and the aformentioned changer are available.

Also fixed a bug where (align:) would override (box:)'s width with its max-width.
parent 7dffab3fe719
......@@ -175,6 +175,7 @@ Documentation is at http://twine2.neocities.org/. See below for compilation inst
* Added a `(text-rotate-x:)` and `(text-rotate-y:)`, which are 3D versions of `(text-rotate:)`, rotating the hook around the X or Y axis, making it appear to lean into the page with perspective.
* Added the changer macros `(border:)`, `(border-size:)`, and `(border-colour:)` (aliases `(b4r:)`, `(b4r-size:)`, `(b4r-colour:)`) which add and adjust CSS borders for hooks. `(border:)` will automatically make the attached hook have `display:inline-block`, so that it remains rectangular and the border can properly enclose it.
* Added a `(corner-radius:)` macro, which rounds the corners of the hook (using the CSS "border-radius" property, which, despite its name, works on elements without borders). It will also add padding, proportional to the amount of corner rounding, so that the corners don't encroach on the inner text.
* Added a `(button:)` macro, which is designed especially for links. It is essentially a convenient shorthand for `(align:"=><=")+(box:"X")+(border:"solid")+(css:"padding:0px")+(corner-radius:16)`.
* The following `(text-style:)` styles have been added:
* "double-underline", "wavy-underline", "double-strike" and "wavy-strike", which are variants of "underline" and "strike". Be aware these will not work in Internet Explorer.
* "fidget", which jolts the hook by one pixel in cardinal directions pseudorandomly.
......
......@@ -560,17 +560,18 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'engine', 'datatypes/c
[String, optional(String)]
)
/*d:
(link-storylet: [String], Number or Lambda) -> Command
(link-storylet: [String], Number or Lambda, [String]) -> Command
If there are storylets (passages containing a (storylet:) call) in this story, this will create a link to the first open storylet that
matches the passed-in 'where' lambda, or, if a number **n** was passed in, the **n**th (or, if negative, **n**thlast) open storylet.
An optional final string can provide text to display when no such storylet is open currently.
Example usage:
```
You look over the paddock as you ponder how you'll spend the coming day.
* (link-storylet: 1)
* (link-storylet: 2)
* (link-storylet: 3)
You look over the paddock as you ponder three ways you may spend the coming day.
* (link-storylet: 1, "//(Unavailable)//")
* (link-storylet: 2, "//(Unavailable)//")
* (link-storylet: 3, "//(Unavailable)//")
```
Rationale:
......@@ -584,7 +585,7 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'engine', 'datatypes/c
will be the one with the *least* (urgency:) value among open storylets). If the number 0 is given, an error will result.
If there is no storylet available for the link (such as `(link-storylet: 6)` when only 4 storylets are currently open) then
nothing will be displayed. An error will NOT be produced.
the optional final string will be displayed. If it isn't given, nothing will be displayed.
Note that (link-storylet:), unlike (link:), doesn't accept a changer value to style the produced link. This is because, as
this produces a command (and not a changer), you can simply attach changers to the front of it to style the link.
......@@ -596,27 +597,17 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'engine', 'datatypes/c
/*
Return a new (link-storylet:) object.
*/
(text, condition) => {
const oneNumber = 'one index number or one \'where\' lambda';
if (condition !== undefined && typeof text !== 'string') {
return TwineError.create('datatype', '(link-storylet:) should be given, after the link text string, only ' + oneNumber + '.');
}
if (condition === 0 || text === 0) {
return TwineError.create('datatype', "(link-storylet:) can't link to the 0th storylet.");
}
if (typeof text === 'string' && !condition) {
return TwineError.create('datatype', '(link-storylet:) must have ' + oneNumber + ' after the link text string');
(...args) => {
const condition = args[args.length === 1 || typeof args[0] !== 'string' ? 0 : 1];
if (!condition || typeof condition === 'string') {
return TwineError.create('datatype', '(link-storylet:) should be given one index number or one \'where\' lambda, after the optional link text string.');
}
},
(cd, section, text, condition) => {
/*
If the text argument is skipped (that is, the condition is in the "text" variable)
then rearrange the variables correctly.
*/
if (!condition) {
condition = text;
text = '';
}
(cd, section, ...args) => {
const condition = args[args.length === 1 ? 0 : args.length === 3 || typeof args[0] === "string" ? 1 : 2];
let text = typeof args[0] === "string" && args[0];
const unavailableText = args[args.length - 1] !== condition ? args[args.length - 1] : false;
/*
Mistakenly attaching the wrong (t8n:) to a (link-storylet:) macro will lead to
a helpful advisory error being emitted.
......@@ -640,35 +631,45 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'engine', 'datatypes/c
The index is, of course, 1-indexed.
*/
condition < 0 ? storylets.length + condition : condition - 1];
if (!passage) {
return cd;
}
passage = passage.get('name');
/*
If the optional text wasn't given, use the passage name for the text.
*/
text = text || passage;
let source;
/*
Create the link in a manner consistent with (link-goto:).
If there is no passage fitting the index or lambda, either return if there's no "unavailable" text,
or use it as the source instead of constructing the <tw-link>.
*/
source = source || '<tw-link tabindex=0 '
if (!passage) {
if (!unavailableText) {
return cd;
}
source = unavailableText;
}
else {
passage = passage.get('name');
/*
Previously visited passages may be styled differently compared
to unvisited passages.
If the optional text wasn't given, use the passage name for the text.
*/
+ (State.passageNameVisited(passage) > 0 ? 'class="visited" ' : '')
+ '>' + text + '</tw-link>';
/*
Instead, the passage name is stored on the <tw-expression>, to be retrieved by clickEvent() above.
*/
cd.data.linkPassageName = passage;
/*
All links need to store their section as jQuery data, so that clickLinkEvent can
check if the section is blocked (thus preventing clicks).
*/
cd.data.section = section;
text = text || passage;
/*
Create the link in a manner consistent with (link-goto:).
*/
source = source || '<tw-link tabindex=0 '
/*
Previously visited passages may be styled differently compared
to unvisited passages.
*/
+ (State.passageNameVisited(passage) > 0 ? 'class="visited" ' : '')
+ '>' + text + '</tw-link>';
/*
Instead, the passage name is stored on the <tw-expression>, to be retrieved by clickEvent() above.
*/
cd.data.linkPassageName = passage;
/*
All links need to store their section as jQuery data, so that clickLinkEvent can
check if the section is blocked (thus preventing clicks).
*/
cd.data.section = section;
}
return assign(cd, {
source,
/*
......@@ -679,7 +680,7 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'engine', 'datatypes/c
transitionDeferred: true,
});
},
[either(parseInt, String, Lambda.TypeSignature('where')), optional(either(parseInt, Lambda.TypeSignature('where')))]
[either(parseInt, String, Lambda.TypeSignature('where')), optional(either(parseInt, String, Lambda.TypeSignature('where'))), optional(String)]
)
......
......@@ -929,6 +929,57 @@ define(['jquery','macros', 'utils', 'utils/renderutils', 'datatypes/colour', 'da
[insensitiveSet(...validT8ns)]
)
/*d:
(button:) -> Changer
When applied to a link, this changer styles it so that it resembles a button, and makes it take up the entire passage width. It is not
recommended that this be used on non-link hooks.
Example usage:
* `(button:)[[Go to the cemetery]]` applies the button style to a single passage link.
* `(enchant:?link's 2ndlast + ?link's last, (button:))` enchants the second-last and last links in the passage with the button style.
Rationale:
Harlowe links, by default, are designed to appear inside and among prose, in the manner of HTML prose. That being said, a story written in a more
traditional interactive fiction style will often want to finish a passage with a series of exit links. These links can benefit from being more
visually prominent and inviting, rather than just single fragments of text. The (button:) changer provides links with a styling that is more typical
of other interactive fiction engines' link options.
Details:
This is essentially a shortcut for a number of other changers added together. `(link: "Link Text", (button:))` is equivalent to
`(link:"Link Text",(align:"=><=")+(box:"X")+(b4r:"solid")+(css:"padding:0px")+(corner-radius:16))`. However, unlike the latter,
this changer is designed to work correctly with (click:) and `(enchant:"text")`, so that the button border matches the current link colour.
To make (button:) links appear in two or more columns, or make two (button:) links appear side-by-side, consider using the column markup.
This changer can be provided to non-link hooks or commands, but since the result will have the same borders and spacing as a button while not
being clickable, it is not recommended to use it this way.
This changer adds the class "enchantment-button" to <tw-link> and <tw-enchantment> elements.
See also:
(align:), (border:), (box:), (corner-radius:)
Added in: 3.2.0
#styling
*/
("button",
() => ChangerCommand.create("button", []),
(d) => {
/*
In order for this to be validly considered usable with (enchant:), it should only have an "attr" item instead of a "functions" item.
This is currently the only way for a ChangeDescriptor to modify an element's class... as uncomfortable as it is.
*/
d.attr.push({
class() {
return this.className + (this.classList.contains('enchantment-button') ? '' : ' '.repeat(this.className.length > 0) + 'enchantment-button');
},
});
return d;
},
[]
)
/*d:
(border: String, [String], [String], [String]) -> Changer
Also known as: (b4r:)
......@@ -2247,6 +2298,7 @@ define(['jquery','macros', 'utils', 'utils/renderutils', 'datatypes/colour', 'da
const styles = {
display: "block",
width: size + boxUnits,
"max-width": size + boxUnits,
[name === "box" ? "margin-left" : "left"]: marginLeft + boxUnits,
"box-sizing": "content-box",
"overflow-y": "auto",
......
......@@ -125,3 +125,24 @@ tw-broken-link {
border: transparent 2px solid;
border-radius: 0.2em;
}
/*
Button styling is applied in three ways: by (button:) changing normal links and (click:) links as their second argument...
*/
tw-link.enchantment-button,
.enchantment-link.enchantment-button,
/*
...by link changers and (click:) links being enchanted by (enchant:), or link expressions changed by attached (button:) changers...
*/
.enchantment-button:not(.enchantment-link) > tw-link,
.enchantment-button:not(.enchantment-link) > .enchantment-link,
/*
...and links inside (dialog:) dialogs.
*/
tw-dialog-links tw-link {
border-radius: 16px;
border-style: solid;
border-width: 2px;
text-align:center;
display:block;
}
......@@ -952,6 +952,23 @@ describe("style changer macros", function() {
});
});
});
describe("the (button:) macro", function() {
it("requires 0 arguments", function() {
expect("(print:(button:))").not.markupToError();
expect("(print:(button:1))").markupToError();
});
it("gives the enchanted elements the 'enchantment-button' class", function() {
var p = runPassage("(link:'foo',(button:))[]");
expect(p.find('tw-link.enchantment-button').length).toBe(1);
p = runPassage("(button:)[[Test]]","Test");
expect(p.find('tw-expression.enchantment-button').length).toBe(1);
});
it("works with (enchant:) given ?Link", function() {
var p = runPassage("(enchant:?Link,(button:))[[Test]]","Test");
expect(p.find('tw-enchantment.enchantment-button > tw-link').length).toBe(1);
});
// TODO:CSS tests
});
describe("the (collapse:) macro", function() {
function tests(expect) {
it("eliminates runs of whitespace between { and }", function() {
......
......@@ -46,7 +46,7 @@ describe("metadata macros", function() {
});
});
describe("the (link-storylet:) macro", function() {
it("accepts optional link text, plus a non-zero whole number or a 'where' lambda", function() {
it("accepts optional link text, plus a non-zero whole number or a 'where' lambda, plus optional unavailable text", function() {
expect("(link-storylet: 'ears')").markupToError();
expect("(link-storylet:)").markupToError();
expect("(link-storylet: 0)").markupToError();
......@@ -56,6 +56,8 @@ describe("metadata macros", function() {
expect("(link-storylet: 'ears', 2)").not.markupToError();
expect("(link-storylet: 'ears', where its tags contains 'e')").not.markupToError();
expect("(link-storylet: 'ears', 2, where its tags contains 'e')").markupToError();
expect("(link-storylet: 'ears', where its tags contains 'e', 'foo')").not.markupToError();
expect("(link-storylet: 'ears', 2, 'bar')").not.markupToError();
});
it("given an index n, creates a link to the nth open storylet, using the same order as (open-storylets:)", function() {
createPassage("**foobarbaz(storylet: when true is true)**", "grault");
......@@ -90,11 +92,17 @@ describe("metadata macros", function() {
expect("(set:$a to 1)(link-storylet: where its name contains 't')").markupToPrint('grault');
expect("(set:$a to 2)(link-storylet: where its name contains 'x')").markupToPrint('quux');
});
it("if there are no matches, prints nothing", function() {
it("if there are no matches, prints the unavailable text, if given, or nothing", function() {
createPassage("(storylet: when $a is > 1)", "corge");
createPassage("(storylet: when $a is > 1)", "quux");
expect("(set:$a to 1)(link-storylet: 2)").markupToPrint('');
expect("(set:$a to 2)(link-storylet: where its name is 'qux')").markupToPrint('');
var p = runPassage("(set:$a to 1)(link-storylet: 2, 'foobaz')");
expect(p.text()).toBe('foobaz');
expect(p.find('tw-link').length).toBe(0);
p = runPassage("(set:$a to 2)(link-storylet: where its name is 'qux', 'foobaz')");
expect(p.text()).toBe('foobaz');
expect(p.find('tw-link').length).toBe(0);
});
it("uses the optional string as the link text instead of the passage name", function() {
createPassage("(storylet: when $a is > 1)", "corge");
......
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