squiffy-runtime 6.0.0-alpha.2 → 6.0.0-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/dist/animation.d.ts +11 -0
- package/dist/events.d.ts +20 -0
- package/dist/import.d.ts +4 -0
- package/dist/linkHandler.d.ts +8 -0
- package/dist/pluginManager.d.ts +23 -0
- package/dist/squiffy.runtime.d.ts +3 -34
- package/dist/squiffy.runtime.global.js +126 -0
- package/dist/squiffy.runtime.global.js.map +1 -0
- package/dist/squiffy.runtime.js +8779 -547
- package/dist/squiffy.runtime.js.map +1 -0
- package/dist/squiffy.runtime.test.d.ts +1 -0
- package/dist/state.d.ts +19 -0
- package/dist/textProcessor.d.ts +11 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.plugins.d.ts +27 -0
- package/dist/updater.d.ts +2 -0
- package/dist/utils.d.ts +1 -0
- package/package.json +26 -5
- package/src/__snapshots__/squiffy.runtime.test.ts.snap +138 -0
- package/src/animation.ts +68 -0
- package/src/events.ts +41 -0
- package/src/import.ts +5 -0
- package/src/linkHandler.ts +18 -0
- package/src/pluginManager.ts +74 -0
- package/src/plugins/animate.ts +97 -0
- package/src/plugins/index.ts +13 -0
- package/src/plugins/live.ts +83 -0
- package/src/plugins/random.ts +22 -0
- package/src/plugins/replaceLabel.ts +22 -0
- package/src/plugins/rotateSequence.ts +61 -0
- package/src/squiffy.runtime.test.ts +677 -0
- package/src/squiffy.runtime.ts +528 -499
- package/src/state.ts +106 -0
- package/src/textProcessor.ts +76 -0
- package/src/types.plugins.ts +41 -0
- package/src/types.ts +81 -0
- package/src/updater.ts +77 -0
- package/src/utils.ts +17 -0
- package/tsconfig.json +4 -1
- package/vite.config.ts +36 -0
- package/vitest.config.ts +9 -0
- package/vitest.setup.ts +16 -0
package/src/squiffy.runtime.ts
CHANGED
|
@@ -1,188 +1,129 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
interface SquiffyApi {
|
|
16
|
-
restart: () => void;
|
|
17
|
-
get: (attribute: string) => any;
|
|
18
|
-
set: (attribute: string, value: any) => void;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface Story {
|
|
22
|
-
js: (() => void)[];
|
|
23
|
-
start: string;
|
|
24
|
-
id?: string | null;
|
|
25
|
-
sections: Record<string, Section>;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface Section {
|
|
29
|
-
text?: string;
|
|
30
|
-
clear?: boolean;
|
|
31
|
-
attributes?: string[],
|
|
32
|
-
jsIndex?: number;
|
|
33
|
-
passages?: Record<string, Passage>;
|
|
34
|
-
passageCount?: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface Passage {
|
|
38
|
-
text?: string;
|
|
39
|
-
clear?: boolean;
|
|
40
|
-
attributes?: string[];
|
|
41
|
-
jsIndex?: number;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export const init = (options: SquiffyInitOptions): SquiffyApi => {
|
|
1
|
+
import { SquiffyApi, SquiffyInitOptions, Story, Section, Passage } from "./types.js";
|
|
2
|
+
import { TextProcessor } from "./textProcessor.js";
|
|
3
|
+
import { Emitter, SquiffyEventMap } from "./events.js";
|
|
4
|
+
import { State } from "./state.js";
|
|
5
|
+
import { updateStory } from "./updater.js";
|
|
6
|
+
import { PluginManager } from "./pluginManager.js";
|
|
7
|
+
import { Plugins } from "./plugins/index.js";
|
|
8
|
+
import { LinkHandler } from "./linkHandler.js";
|
|
9
|
+
import { Animation } from "./animation.js";
|
|
10
|
+
import { imports } from "./import.js";
|
|
11
|
+
|
|
12
|
+
export type { SquiffyApi } from "./types.js";
|
|
13
|
+
|
|
14
|
+
export const init = async (options: SquiffyInitOptions): Promise<SquiffyApi> => {
|
|
45
15
|
let story: Story;
|
|
46
16
|
let currentSection: Section;
|
|
47
17
|
let currentSectionElement: HTMLElement;
|
|
18
|
+
let currentPassageElement: HTMLElement;
|
|
19
|
+
let currentBlockOutputElement: HTMLElement;
|
|
48
20
|
let scrollPosition = 0;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
let
|
|
52
|
-
|
|
53
|
-
function
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
result = localStorage[story.id + '-' + attribute];
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
result = storageFallback[attribute];
|
|
71
|
-
}
|
|
72
|
-
if (!result) return null;
|
|
73
|
-
return JSON.parse(result);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function initLinkHandler() {
|
|
77
|
-
function handleLink(link: HTMLElement) {
|
|
78
|
-
if (link.classList.contains('disabled')) return;
|
|
79
|
-
let passage = link.getAttribute('data-passage');
|
|
80
|
-
let section = link.getAttribute('data-section');
|
|
81
|
-
const rotateAttr = link.getAttribute('data-rotate');
|
|
82
|
-
const sequenceAttr = link.getAttribute('data-sequence');
|
|
83
|
-
const rotateOrSequenceAttr = rotateAttr || sequenceAttr;
|
|
21
|
+
const emitter = new Emitter<SquiffyEventMap>();
|
|
22
|
+
const transitions: (() => Promise<void>)[] = [];
|
|
23
|
+
let runningTransitions = false;
|
|
24
|
+
|
|
25
|
+
async function handleLink(link: HTMLElement): Promise<boolean> {
|
|
26
|
+
if (runningTransitions) return false;
|
|
27
|
+
const outputSection = link.closest(".squiffy-output-section");
|
|
28
|
+
if (outputSection !== currentSectionElement) return false;
|
|
29
|
+
|
|
30
|
+
if (link.classList.contains("disabled")) return false;
|
|
31
|
+
|
|
32
|
+
const passage = link.getAttribute("data-passage");
|
|
33
|
+
const section = link.getAttribute("data-section");
|
|
34
|
+
|
|
35
|
+
if (passage !== null) {
|
|
36
|
+
disableLink(link);
|
|
37
|
+
set("_turncount", get("_turncount") + 1);
|
|
38
|
+
await processLink(link);
|
|
84
39
|
if (passage) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
passage = processLink(passage);
|
|
88
|
-
if (passage) {
|
|
89
|
-
currentSectionElement?.appendChild(document.createElement('hr'));
|
|
90
|
-
showPassage(passage);
|
|
91
|
-
}
|
|
92
|
-
const turnPassage = '@' + get('_turncount');
|
|
93
|
-
if (currentSection.passages) {
|
|
94
|
-
if (turnPassage in currentSection.passages) {
|
|
95
|
-
showPassage(turnPassage);
|
|
96
|
-
}
|
|
97
|
-
if ('@last' in currentSection.passages && get('_turncount') >= (currentSection.passageCount || 0)) {
|
|
98
|
-
showPassage('@last');
|
|
99
|
-
}
|
|
100
|
-
}
|
|
40
|
+
currentBlockOutputElement = null;
|
|
41
|
+
await showPassage(passage);
|
|
101
42
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (section) {
|
|
107
|
-
go(section);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
else if (rotateOrSequenceAttr) {
|
|
111
|
-
const result = rotate(rotateOrSequenceAttr, rotateAttr ? link.innerText : '');
|
|
112
|
-
link.innerHTML = result[0]!.replace(/"/g, '"').replace(/'/g, '\'');
|
|
113
|
-
const dataAttribute = rotateAttr ? 'data-rotate' : 'data-sequence';
|
|
114
|
-
link.setAttribute(dataAttribute, result[1] || '');
|
|
115
|
-
if (!result[1]) {
|
|
116
|
-
disableLink(link);
|
|
43
|
+
const turnPassage = "@" + get("_turncount");
|
|
44
|
+
if (currentSection.passages) {
|
|
45
|
+
if (turnPassage in currentSection.passages) {
|
|
46
|
+
await showPassage(turnPassage);
|
|
117
47
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
set(attribute, result[0]);
|
|
48
|
+
if ("@last" in currentSection.passages && get("_turncount") >= (currentSection.passageCount || 0)) {
|
|
49
|
+
await showPassage("@last");
|
|
121
50
|
}
|
|
122
|
-
save();
|
|
123
51
|
}
|
|
52
|
+
|
|
53
|
+
emitter.emit("linkClick", { linkType: "passage" });
|
|
54
|
+
return true;
|
|
124
55
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
|
|
56
|
+
|
|
57
|
+
if (section !== null) {
|
|
58
|
+
await processLink(link);
|
|
59
|
+
if (section) {
|
|
60
|
+
await go(section);
|
|
130
61
|
}
|
|
62
|
+
|
|
63
|
+
emitter.emit("linkClick", { linkType: "section" });
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const [handled, type, result] = linkHandler.handleLink(link);
|
|
68
|
+
|
|
69
|
+
if (handled) {
|
|
70
|
+
if (result?.disableLink) {
|
|
71
|
+
disableLink(link);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
save();
|
|
75
|
+
|
|
76
|
+
emitter.emit("linkClick", { linkType: type });
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function handleClick(event: Event) {
|
|
84
|
+
const target = event.target as HTMLElement;
|
|
85
|
+
if (target.classList.contains("squiffy-link")) {
|
|
86
|
+
await handleLink(target);
|
|
131
87
|
}
|
|
132
|
-
|
|
133
|
-
document.addEventListener('click', handleClick);
|
|
134
|
-
document.addEventListener('keypress', function (event) {
|
|
135
|
-
if (event.key !== "Enter") return;
|
|
136
|
-
handleClick(event);
|
|
137
|
-
});
|
|
138
88
|
}
|
|
139
89
|
|
|
140
90
|
function disableLink(link: Element) {
|
|
141
|
-
link.classList.add(
|
|
142
|
-
link.setAttribute(
|
|
91
|
+
link.classList.add("disabled");
|
|
92
|
+
link.setAttribute("tabindex", "-1");
|
|
143
93
|
}
|
|
144
|
-
|
|
145
|
-
function
|
|
146
|
-
|
|
94
|
+
|
|
95
|
+
function enableLink(link: Element) {
|
|
96
|
+
link.classList.remove("disabled");
|
|
97
|
+
link.removeAttribute("tabindex");
|
|
147
98
|
}
|
|
148
99
|
|
|
149
|
-
function begin() {
|
|
100
|
+
async function begin() {
|
|
150
101
|
if (!load()) {
|
|
151
|
-
go(story.start);
|
|
102
|
+
await go(story.start);
|
|
152
103
|
}
|
|
153
104
|
}
|
|
154
105
|
|
|
155
|
-
function processLink(link:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
if (first) {
|
|
166
|
-
target = section;
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
setAttribute(section);
|
|
170
|
-
}
|
|
106
|
+
async function processLink(link: HTMLElement) {
|
|
107
|
+
animation.runLinkAnimation(link);
|
|
108
|
+
await runTransitions();
|
|
109
|
+
const settersJson = link.getAttribute("data-set");
|
|
110
|
+
if (settersJson) {
|
|
111
|
+
const setters = JSON.parse(settersJson) as string[];
|
|
112
|
+
for (const attribute of setters) {
|
|
113
|
+
setAttribute(attribute);
|
|
171
114
|
}
|
|
172
|
-
|
|
173
|
-
});
|
|
174
|
-
return target;
|
|
115
|
+
}
|
|
175
116
|
}
|
|
176
117
|
|
|
177
118
|
function setAttribute(expr: string) {
|
|
178
|
-
expr = expr.replace(/^(\w*\s*):=(.*)$/, (_, name, value) => (name + "=" + ui.processText(value)));
|
|
119
|
+
expr = expr.replace(/^(\w*\s*):=(.*)$/, (_, name, value) => (name + "=" + ui.processText(value, true)));
|
|
179
120
|
const setRegex = /^([\w]*)\s*=\s*(.*)$/;
|
|
180
121
|
const setMatch = setRegex.exec(expr);
|
|
181
122
|
if (setMatch) {
|
|
182
123
|
const lhs = setMatch[1];
|
|
183
124
|
let rhs = setMatch[2];
|
|
184
125
|
if (isNaN(rhs as any)) {
|
|
185
|
-
if (startsWith(
|
|
126
|
+
if (rhs.startsWith("@")) rhs = get(rhs.substring(1));
|
|
186
127
|
set(lhs, rhs);
|
|
187
128
|
}
|
|
188
129
|
else {
|
|
@@ -190,33 +131,33 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
|
|
|
190
131
|
}
|
|
191
132
|
}
|
|
192
133
|
else {
|
|
193
|
-
const incDecRegex = /^([\w]*)\s*([
|
|
134
|
+
const incDecRegex = /^([\w]*)\s*([+\-*/])=\s*(.*)$/;
|
|
194
135
|
const incDecMatch = incDecRegex.exec(expr);
|
|
195
136
|
if (incDecMatch) {
|
|
196
137
|
const lhs = incDecMatch[1];
|
|
197
138
|
const op = incDecMatch[2];
|
|
198
139
|
let rhs = incDecMatch[3];
|
|
199
|
-
if (startsWith(
|
|
140
|
+
if (rhs.startsWith("@")) rhs = get(rhs.substring(1));
|
|
200
141
|
const rhsNumeric = parseFloat(rhs);
|
|
201
142
|
let value = get(lhs);
|
|
202
143
|
if (value === null) value = 0;
|
|
203
|
-
if (op ==
|
|
144
|
+
if (op == "+") {
|
|
204
145
|
value += rhsNumeric;
|
|
205
146
|
}
|
|
206
|
-
if (op ==
|
|
147
|
+
if (op == "-") {
|
|
207
148
|
value -= rhsNumeric;
|
|
208
149
|
}
|
|
209
|
-
if (op ==
|
|
150
|
+
if (op == "*") {
|
|
210
151
|
value *= rhsNumeric;
|
|
211
152
|
}
|
|
212
|
-
if (op ==
|
|
153
|
+
if (op == "/") {
|
|
213
154
|
value /= rhsNumeric;
|
|
214
155
|
}
|
|
215
156
|
set(lhs, value);
|
|
216
157
|
}
|
|
217
158
|
else {
|
|
218
159
|
let value = true;
|
|
219
|
-
if (startsWith(
|
|
160
|
+
if (expr.startsWith("not ")) {
|
|
220
161
|
expr = expr.substring(4);
|
|
221
162
|
value = false;
|
|
222
163
|
}
|
|
@@ -224,123 +165,143 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
|
|
|
224
165
|
}
|
|
225
166
|
}
|
|
226
167
|
}
|
|
227
|
-
|
|
228
|
-
function
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const label = match[1];
|
|
233
|
-
let text = match[2];
|
|
234
|
-
if (currentSection.passages && text in currentSection.passages) {
|
|
235
|
-
text = currentSection.passages[text].text || '';
|
|
236
|
-
}
|
|
237
|
-
else if (text in story.sections) {
|
|
238
|
-
text = story.sections[text].text || '';
|
|
239
|
-
}
|
|
240
|
-
const stripParags = /^<p>(.*)<\/p>$/;
|
|
241
|
-
const stripParagsMatch = stripParags.exec(text);
|
|
242
|
-
if (stripParagsMatch) {
|
|
243
|
-
text = stripParagsMatch[1];
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const labelElement = outputElement.querySelector('.squiffy-label-' + label);
|
|
247
|
-
if (!labelElement) return;
|
|
248
|
-
|
|
249
|
-
labelElement.addEventListener('transitionend', function () {
|
|
250
|
-
labelElement.innerHTML = ui.processText(text);
|
|
251
|
-
|
|
252
|
-
labelElement.addEventListener('transitionend', function () {
|
|
253
|
-
save();
|
|
254
|
-
}, { once: true });
|
|
255
|
-
|
|
256
|
-
labelElement.classList.remove('fade-out');
|
|
257
|
-
labelElement.classList.add('fade-in');
|
|
258
|
-
}, { once: true });
|
|
259
|
-
|
|
260
|
-
labelElement.classList.add('fade-out');
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function go(section: string) {
|
|
264
|
-
set('_transition', null);
|
|
265
|
-
newSection();
|
|
266
|
-
currentSection = story.sections[section];
|
|
168
|
+
|
|
169
|
+
async function go(sectionName: string) {
|
|
170
|
+
const oldCanGoBack = canGoBack();
|
|
171
|
+
newSection(sectionName);
|
|
172
|
+
currentSection = story.sections[sectionName];
|
|
267
173
|
if (!currentSection) return;
|
|
268
|
-
set(
|
|
269
|
-
setSeen(
|
|
270
|
-
const master = story.sections[
|
|
174
|
+
set("_section", sectionName);
|
|
175
|
+
state.setSeen(sectionName);
|
|
176
|
+
const master = story.sections[""];
|
|
177
|
+
if (master?.clear || currentSection.clear) {
|
|
178
|
+
clearScreen();
|
|
179
|
+
}
|
|
271
180
|
if (master) {
|
|
272
|
-
run(master);
|
|
273
|
-
ui.write(master.text || '');
|
|
181
|
+
await run(master, "[[]]");
|
|
274
182
|
}
|
|
275
|
-
run(currentSection);
|
|
183
|
+
await run(currentSection, `[[${sectionName}]]`);
|
|
276
184
|
// The JS might have changed which section we're in
|
|
277
|
-
if (get(
|
|
278
|
-
set(
|
|
279
|
-
|
|
185
|
+
if (get("_section") == sectionName) {
|
|
186
|
+
set("_turncount", 0);
|
|
187
|
+
writeUndoLog();
|
|
280
188
|
save();
|
|
281
189
|
}
|
|
190
|
+
const newCanGoBack = canGoBack();
|
|
191
|
+
if (newCanGoBack != oldCanGoBack) {
|
|
192
|
+
emitter.emit("canGoBackChanged", { canGoBack: newCanGoBack });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function runJs(index: number, extra: any = null) {
|
|
197
|
+
const squiffy = {
|
|
198
|
+
get: get,
|
|
199
|
+
set: set,
|
|
200
|
+
ui: {
|
|
201
|
+
transition: addTransition,
|
|
202
|
+
write: ui.write,
|
|
203
|
+
scrollToEnd: ui.scrollToEnd,
|
|
204
|
+
},
|
|
205
|
+
story: {
|
|
206
|
+
go: go,
|
|
207
|
+
},
|
|
208
|
+
element: outputElementContainer,
|
|
209
|
+
import: imports,
|
|
210
|
+
...extra
|
|
211
|
+
};
|
|
212
|
+
story.js[index](squiffy, get, set);
|
|
282
213
|
}
|
|
283
214
|
|
|
284
|
-
function run(section: Section) {
|
|
285
|
-
if (section.clear) {
|
|
286
|
-
ui.clearScreen();
|
|
287
|
-
}
|
|
215
|
+
async function run(section: Section, source: string) {
|
|
288
216
|
if (section.attributes) {
|
|
289
|
-
processAttributes(section.attributes
|
|
217
|
+
await processAttributes(section.attributes);
|
|
290
218
|
}
|
|
291
219
|
if (section.jsIndex !== undefined) {
|
|
292
|
-
|
|
220
|
+
runJs(section.jsIndex);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
ui.write(section.text || "", source);
|
|
224
|
+
|
|
225
|
+
await runTransitions();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function runTransitions() {
|
|
229
|
+
runningTransitions = true;
|
|
230
|
+
currentSectionElement.classList.add("links-disabled");
|
|
231
|
+
for (const transition of transitions) {
|
|
232
|
+
await transition();
|
|
293
233
|
}
|
|
234
|
+
transitions.length = 0;
|
|
235
|
+
runningTransitions = false;
|
|
236
|
+
currentSectionElement.classList.remove("links-disabled");
|
|
294
237
|
}
|
|
295
238
|
|
|
296
|
-
function showPassage(passageName: string) {
|
|
239
|
+
async function showPassage(passageName: string) {
|
|
240
|
+
const oldCanGoBack = canGoBack();
|
|
297
241
|
let passage = currentSection.passages && currentSection.passages[passageName];
|
|
298
|
-
const masterSection = story.sections[
|
|
242
|
+
const masterSection = story.sections[""];
|
|
299
243
|
if (!passage && masterSection && masterSection.passages) passage = masterSection.passages[passageName];
|
|
300
|
-
if (!passage)
|
|
301
|
-
|
|
244
|
+
if (!passage) {
|
|
245
|
+
throw `No passage named ${passageName} in the current section or master section`;
|
|
246
|
+
}
|
|
247
|
+
state.setSeen(passageName);
|
|
248
|
+
|
|
249
|
+
const passages: Passage[] = [];
|
|
250
|
+
const runFns: (() => Promise<void>)[] = [];
|
|
251
|
+
|
|
302
252
|
if (masterSection && masterSection.passages) {
|
|
303
|
-
const masterPassage = masterSection.passages[
|
|
253
|
+
const masterPassage = masterSection.passages[""];
|
|
304
254
|
if (masterPassage) {
|
|
305
|
-
|
|
306
|
-
|
|
255
|
+
passages.push(masterPassage);
|
|
256
|
+
runFns.push(() => run(masterPassage, "[[]][]"));
|
|
307
257
|
}
|
|
308
258
|
}
|
|
309
|
-
|
|
259
|
+
|
|
260
|
+
const master = currentSection.passages && currentSection.passages[""];
|
|
310
261
|
if (master) {
|
|
311
|
-
|
|
312
|
-
|
|
262
|
+
passages.push(master);
|
|
263
|
+
runFns.push(() => run(master, `[[${get("_section")}]][]`));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
passages.push(passage);
|
|
267
|
+
runFns.push(() => run(passage, `[[${get("_section")}]][${passageName}]`));
|
|
268
|
+
|
|
269
|
+
if (passages.some(p => p.clear)) {
|
|
270
|
+
clearScreen();
|
|
271
|
+
createSectionElement();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
currentPassageElement = document.createElement("div");
|
|
275
|
+
currentPassageElement.classList.add("squiffy-output-passage");
|
|
276
|
+
currentPassageElement.setAttribute("data-passage", `${passageName}`);
|
|
277
|
+
|
|
278
|
+
currentSectionElement.appendChild(currentPassageElement);
|
|
279
|
+
currentBlockOutputElement = null;
|
|
280
|
+
|
|
281
|
+
for (const fn of runFns) {
|
|
282
|
+
await fn();
|
|
313
283
|
}
|
|
314
|
-
|
|
315
|
-
|
|
284
|
+
|
|
285
|
+
writeUndoLog();
|
|
316
286
|
save();
|
|
287
|
+
const newCanGoBack = canGoBack();
|
|
288
|
+
if (newCanGoBack != oldCanGoBack) {
|
|
289
|
+
emitter.emit("canGoBackChanged", { canGoBack: newCanGoBack });
|
|
290
|
+
}
|
|
317
291
|
}
|
|
318
292
|
|
|
319
|
-
function processAttributes(attributes: string[]) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
else {
|
|
325
|
-
setAttribute(attribute);
|
|
326
|
-
}
|
|
327
|
-
});
|
|
293
|
+
async function processAttributes(attributes: string[]) {
|
|
294
|
+
for (const attribute of attributes) {
|
|
295
|
+
setAttribute(attribute);
|
|
296
|
+
}
|
|
328
297
|
}
|
|
329
298
|
|
|
330
299
|
function restart() {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
else {
|
|
340
|
-
storageFallback = {};
|
|
341
|
-
}
|
|
342
|
-
if (settings.scroll === 'element') {
|
|
343
|
-
outputElement.innerHTML = '';
|
|
300
|
+
state.reset();
|
|
301
|
+
// TODO: This feels like the wrong way of triggering location.reload()
|
|
302
|
+
// - should be a separate setting to the scroll setting.
|
|
303
|
+
if (settings.scroll === "element" || settings.scroll === "none") {
|
|
304
|
+
outputElement.innerHTML = "";
|
|
344
305
|
begin();
|
|
345
306
|
}
|
|
346
307
|
else {
|
|
@@ -349,94 +310,162 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
|
|
|
349
310
|
}
|
|
350
311
|
|
|
351
312
|
function save() {
|
|
352
|
-
|
|
313
|
+
// TODO: Queue up all attribute changes and save them only when this is called
|
|
314
|
+
set("_output", outputElement.innerHTML);
|
|
353
315
|
}
|
|
354
316
|
|
|
355
317
|
function load() {
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
318
|
+
const runUiJs = () => {
|
|
319
|
+
if (story.uiJsIndex !== undefined) {
|
|
320
|
+
runJs(story.uiJsIndex, {
|
|
321
|
+
registerAnimation: animation.registerAnimation.bind(animation),
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
state.load();
|
|
327
|
+
const output = get("_output");
|
|
328
|
+
if (!output) {
|
|
329
|
+
runUiJs();
|
|
330
|
+
return false;
|
|
366
331
|
}
|
|
332
|
+
|
|
333
|
+
outputElement.innerHTML = output;
|
|
334
|
+
|
|
335
|
+
setCurrentSectionElement();
|
|
336
|
+
setCurrentPassageElement();
|
|
337
|
+
currentBlockOutputElement = outputElement.querySelector(".squiffy-output-block:last-child");
|
|
338
|
+
|
|
339
|
+
currentSection = story.sections[get("_section")];
|
|
340
|
+
runUiJs();
|
|
341
|
+
pluginManager.onLoad();
|
|
367
342
|
return true;
|
|
368
343
|
}
|
|
369
|
-
|
|
370
|
-
function
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
seenSections.push(sectionName);
|
|
375
|
-
set('_seen_sections', seenSections);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function seen(sectionName: string) {
|
|
380
|
-
const seenSections = get('_seen_sections');
|
|
381
|
-
if (!seenSections) return false;
|
|
382
|
-
return (seenSections.indexOf(sectionName) > -1);
|
|
344
|
+
|
|
345
|
+
function newBlockOutputElement() {
|
|
346
|
+
currentBlockOutputElement = document.createElement("div");
|
|
347
|
+
currentBlockOutputElement.classList.add("squiffy-output-block");
|
|
348
|
+
(currentPassageElement || currentSectionElement)?.appendChild(currentBlockOutputElement);
|
|
383
349
|
}
|
|
384
350
|
|
|
385
|
-
function newSection() {
|
|
351
|
+
function newSection(sectionName?: string) {
|
|
386
352
|
if (currentSectionElement) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const attribute = el.getAttribute('data-attribute') || el.id;
|
|
353
|
+
currentSectionElement.querySelectorAll("input").forEach(el => {
|
|
354
|
+
const attribute = el.getAttribute("data-attribute") || el.id;
|
|
390
355
|
if (attribute) set(attribute, el.value);
|
|
391
356
|
el.disabled = true;
|
|
392
357
|
});
|
|
393
358
|
|
|
394
359
|
currentSectionElement.querySelectorAll("[contenteditable]").forEach(el => {
|
|
395
|
-
const attribute = el.getAttribute(
|
|
360
|
+
const attribute = el.getAttribute("data-attribute") || el.id;
|
|
396
361
|
if (attribute) set(attribute, el.innerHTML);
|
|
397
|
-
(el as HTMLElement).contentEditable =
|
|
362
|
+
(el as HTMLElement).contentEditable = "false";
|
|
398
363
|
});
|
|
399
364
|
|
|
400
|
-
currentSectionElement.querySelectorAll(
|
|
401
|
-
const attribute = el.getAttribute(
|
|
365
|
+
currentSectionElement.querySelectorAll("textarea").forEach(el => {
|
|
366
|
+
const attribute = el.getAttribute("data-attribute") || el.id;
|
|
402
367
|
if (attribute) set(attribute, el.value);
|
|
403
368
|
el.disabled = true;
|
|
404
369
|
});
|
|
405
370
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
371
|
+
|
|
372
|
+
currentPassageElement = null;
|
|
373
|
+
createSectionElement(sectionName);
|
|
374
|
+
newBlockOutputElement();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function createSectionElement(sectionName?: string) {
|
|
378
|
+
currentSectionElement = document.createElement("div");
|
|
379
|
+
currentSectionElement.classList.add("squiffy-output-section");
|
|
380
|
+
currentSectionElement.setAttribute("data-section", sectionName ?? get("_section"));
|
|
381
|
+
if (!sectionName) {
|
|
382
|
+
currentSectionElement.setAttribute("data-clear", "true");
|
|
383
|
+
}
|
|
413
384
|
outputElement.appendChild(currentSectionElement);
|
|
414
|
-
|
|
415
|
-
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function getClearStack() {
|
|
388
|
+
return outputElement.querySelector<HTMLElement>(".squiffy-clear-stack");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function clearScreen() {
|
|
392
|
+
// Callers should call createSectionElement() after calling this function, so there's a place for new
|
|
393
|
+
// output to go. We don't call this automatically within this function, in case we're just about to create
|
|
394
|
+
// a new section anyway.
|
|
395
|
+
let clearStack = getClearStack();
|
|
396
|
+
if (!clearStack) {
|
|
397
|
+
clearStack = document.createElement("div");
|
|
398
|
+
clearStack.classList.add("squiffy-clear-stack");
|
|
399
|
+
clearStack.style.display = "none";
|
|
400
|
+
outputElement.prepend(clearStack);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const clearStackItem = document.createElement("div");
|
|
404
|
+
clearStack.appendChild(clearStackItem);
|
|
405
|
+
|
|
406
|
+
// Move everything in the outputElement (except the clearStack itself) into the new clearStackItem
|
|
407
|
+
for (const child of [...outputElement.children]) {
|
|
408
|
+
if (child !== clearStack) {
|
|
409
|
+
clearStackItem.appendChild(child);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// NOTE: If we offer an option to disable the "back" feature, all of the above can be replaced with:
|
|
414
|
+
// outputElement.innerHTML = '';
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function unClearScreen() {
|
|
418
|
+
const clearStack = getClearStack();
|
|
419
|
+
for (const child of [...outputElement.children]) {
|
|
420
|
+
if (child !== clearStack) {
|
|
421
|
+
child.remove();
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const clearStackItem = clearStack.children[clearStack.children.length - 1];
|
|
426
|
+
for (const child of [...clearStackItem.children]) {
|
|
427
|
+
outputElement.appendChild(child);
|
|
428
|
+
}
|
|
429
|
+
clearStackItem.remove();
|
|
416
430
|
}
|
|
417
431
|
|
|
418
432
|
const ui = {
|
|
419
|
-
write: (text: string) => {
|
|
420
|
-
if (!currentSectionElement) return;
|
|
433
|
+
write: (text: string, source: string) => {
|
|
421
434
|
scrollPosition = outputElement.scrollHeight;
|
|
422
|
-
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
435
|
+
|
|
436
|
+
const html = ui.processText(text, false).trim();
|
|
437
|
+
|
|
438
|
+
// Previously, we skipped the rest of this if "html" came back as an empty string.
|
|
439
|
+
// But, we _do_ always want the block to be created, in the editor at least - as the
|
|
440
|
+
// author might be in the middle of an edit. When they start writing text for this
|
|
441
|
+
// source (section or passage), we want it to appear in the right place.
|
|
442
|
+
// TODO: What if this comes from a master section/passage though, and there's no
|
|
443
|
+
// text (just a script)? Or if there's conditional text that doesn't display?
|
|
444
|
+
|
|
445
|
+
if (!currentBlockOutputElement) {
|
|
446
|
+
newBlockOutputElement();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const div = document.createElement("div");
|
|
450
|
+
if (source) {
|
|
451
|
+
div.setAttribute("data-source", source);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
div.innerHTML = html;
|
|
455
|
+
pluginManager.onWrite(div);
|
|
456
|
+
currentBlockOutputElement.appendChild(div);
|
|
427
457
|
ui.scrollToEnd();
|
|
428
458
|
},
|
|
429
459
|
clearScreen: () => {
|
|
430
|
-
|
|
431
|
-
|
|
460
|
+
clearScreen();
|
|
461
|
+
createSectionElement();
|
|
432
462
|
},
|
|
433
463
|
scrollToEnd: () => {
|
|
434
|
-
if (settings.scroll ===
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
464
|
+
if (settings.scroll === "none") {
|
|
465
|
+
// do nothing
|
|
466
|
+
}
|
|
467
|
+
else if (settings.scroll === "element") {
|
|
468
|
+
outputElement.lastElementChild.scrollIntoView({ block: "end", inline: "nearest", behavior: "smooth" });
|
|
440
469
|
}
|
|
441
470
|
else {
|
|
442
471
|
let scrollTo = scrollPosition;
|
|
@@ -444,208 +473,139 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
|
|
|
444
473
|
if (scrollTo > currentScrollTop) {
|
|
445
474
|
const maxScrollTop = document.documentElement.scrollHeight - window.innerHeight;
|
|
446
475
|
if (scrollTo > maxScrollTop) scrollTo = maxScrollTop;
|
|
447
|
-
window.scrollTo({ top: scrollTo, behavior:
|
|
476
|
+
window.scrollTo({ top: scrollTo, behavior: "smooth" });
|
|
448
477
|
}
|
|
449
478
|
}
|
|
450
479
|
},
|
|
451
|
-
processText: (text: string) => {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
480
|
+
processText: (text: string, inline: boolean) => {
|
|
481
|
+
return textProcessor.process(text, inline);
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
function update(newStory: Story) {
|
|
486
|
+
if (newStory.start != story.start) {
|
|
487
|
+
story = newStory;
|
|
488
|
+
state.reset();
|
|
489
|
+
outputElement.innerHTML = "";
|
|
490
|
+
go(story.start);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
updateStory(story, newStory, outputElement, ui, disableLink);
|
|
495
|
+
|
|
496
|
+
story = newStory;
|
|
497
|
+
|
|
498
|
+
setCurrentSectionElement();
|
|
499
|
+
setCurrentPassageElement();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function setCurrentSectionElement() {
|
|
503
|
+
// Multiple .squiffy-output-section elements may be "last-child" if some have been moved to the clear-stack,
|
|
504
|
+
// so we want the very last one
|
|
505
|
+
const allSectionElements = outputElement.querySelectorAll<HTMLElement>(".squiffy-output-section:last-child");
|
|
506
|
+
currentSectionElement = allSectionElements[allSectionElements.length - 1];
|
|
507
|
+
const sectionName = currentSectionElement.getAttribute("data-section");
|
|
508
|
+
currentSection = story.sections[sectionName];
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function setCurrentPassageElement() {
|
|
512
|
+
currentPassageElement = currentSectionElement.querySelector(".squiffy-output-passage:last-child");
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function getHistoryCount() {
|
|
516
|
+
const clearStack = getClearStack();
|
|
517
|
+
const sectionPassageCount = outputElement.querySelectorAll(".squiffy-output-section").length
|
|
518
|
+
+ outputElement.querySelectorAll(".squiffy-output-passage").length;
|
|
519
|
+
|
|
520
|
+
if (!clearStack) {
|
|
521
|
+
return sectionPassageCount;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return sectionPassageCount + clearStack.children.length;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function canGoBack() {
|
|
528
|
+
return getHistoryCount() > 1;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function goBack() {
|
|
532
|
+
if (!canGoBack()) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const clearStack = getClearStack();
|
|
537
|
+
|
|
538
|
+
if (currentPassageElement) {
|
|
539
|
+
const currentPassage = currentPassageElement.getAttribute("data-passage");
|
|
540
|
+
doUndo(currentPassageElement.getAttribute("data-undo"));
|
|
541
|
+
currentPassageElement.remove();
|
|
542
|
+
|
|
543
|
+
// If there's nothing left in the outputElement except for an empty section element that
|
|
544
|
+
// was created when the screen was cleared, pop the clear-stack.
|
|
545
|
+
|
|
546
|
+
let hasEmptySection = false;
|
|
547
|
+
let hasOtherElements = false;
|
|
548
|
+
for (const child of outputElement.children) {
|
|
549
|
+
if (child === clearStack) {
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (child.getAttribute("data-clear") == "true" && child.children.length == 0) {
|
|
553
|
+
hasEmptySection = true;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
hasOtherElements = true;
|
|
557
|
+
break;
|
|
494
558
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
else if (startsWith(text, 'else:')) {
|
|
501
|
-
return processTextCommand_Else(text, data);
|
|
502
|
-
}
|
|
503
|
-
else if (startsWith(text, 'label:')) {
|
|
504
|
-
return processTextCommand_Label(text, data);
|
|
505
|
-
}
|
|
506
|
-
else if (/^rotate[: ]/.test(text)) {
|
|
507
|
-
return processTextCommand_Rotate('rotate', text);
|
|
508
|
-
}
|
|
509
|
-
else if (/^sequence[: ]/.test(text)) {
|
|
510
|
-
return processTextCommand_Rotate('sequence', text);
|
|
511
|
-
}
|
|
512
|
-
else if (currentSection.passages && text in currentSection.passages) {
|
|
513
|
-
return process(currentSection.passages[text].text || '', data);
|
|
514
|
-
}
|
|
515
|
-
else if (text in story.sections) {
|
|
516
|
-
return process(story.sections[text].text || '', data);
|
|
517
|
-
}
|
|
518
|
-
else if (startsWith(text, '@') && !startsWith(text, '@replace')) {
|
|
519
|
-
processAttributes(text.substring(1).split(","));
|
|
520
|
-
return "";
|
|
521
|
-
}
|
|
522
|
-
return get(text);
|
|
559
|
+
|
|
560
|
+
if (hasEmptySection && !hasOtherElements) {
|
|
561
|
+
unClearScreen();
|
|
562
|
+
setCurrentSectionElement();
|
|
523
563
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if (colon == -1) {
|
|
529
|
-
return ('{if ' + command + '}');
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const text = command.substring(colon + 1);
|
|
533
|
-
let condition = command.substring(0, colon);
|
|
534
|
-
condition = condition.replace("<", "<");
|
|
535
|
-
const operatorRegex = /([\w ]*)(=|<=|>=|<>|<|>)(.*)/;
|
|
536
|
-
const match = operatorRegex.exec(condition);
|
|
537
|
-
|
|
538
|
-
let result = false;
|
|
539
|
-
|
|
540
|
-
if (match) {
|
|
541
|
-
const lhs = get(match[1]);
|
|
542
|
-
const op = match[2];
|
|
543
|
-
let rhs = match[3];
|
|
544
|
-
|
|
545
|
-
if (startsWith(rhs, '@')) rhs = get(rhs.substring(1));
|
|
546
|
-
|
|
547
|
-
if (op == '=' && lhs == rhs) result = true;
|
|
548
|
-
if (op == '<>' && lhs != rhs) result = true;
|
|
549
|
-
if (op == '>' && lhs > rhs) result = true;
|
|
550
|
-
if (op == '<' && lhs < rhs) result = true;
|
|
551
|
-
if (op == '>=' && lhs >= rhs) result = true;
|
|
552
|
-
if (op == '<=' && lhs <= rhs) result = true;
|
|
553
|
-
}
|
|
554
|
-
else {
|
|
555
|
-
let checkValue = true;
|
|
556
|
-
if (startsWith(condition, 'not ')) {
|
|
557
|
-
condition = condition.substring(4);
|
|
558
|
-
checkValue = false;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
if (startsWith(condition, 'seen ')) {
|
|
562
|
-
result = (seen(condition.substring(5)) == checkValue);
|
|
563
|
-
}
|
|
564
|
-
else {
|
|
565
|
-
let value = get(condition);
|
|
566
|
-
if (value === null) value = false;
|
|
567
|
-
result = (value == checkValue);
|
|
568
|
-
}
|
|
564
|
+
|
|
565
|
+
for (const link of currentSectionElement.querySelectorAll("a.squiffy-link[data-passage]")) {
|
|
566
|
+
if (link.getAttribute("data-passage") == currentPassage) {
|
|
567
|
+
enableLink(link);
|
|
569
568
|
}
|
|
570
|
-
|
|
571
|
-
const textResult = result ? process(text, data) : '';
|
|
572
|
-
|
|
573
|
-
data.lastIf = result;
|
|
574
|
-
return textResult;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
function processTextCommand_Else(section: string, data: any) {
|
|
578
|
-
if (!('lastIf' in data) || data.lastIf) return '';
|
|
579
|
-
const text = section.substring(5);
|
|
580
|
-
return process(text, data);
|
|
581
569
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
570
|
+
|
|
571
|
+
setCurrentPassageElement();
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
doUndo(currentSectionElement.getAttribute("data-undo"));
|
|
575
|
+
currentSectionElement.remove();
|
|
576
|
+
|
|
577
|
+
// If there's nothing left in the outputElement except for the clear-stack, pop it
|
|
578
|
+
let hasOtherElements = false;
|
|
579
|
+
for (const child of [...outputElement.children]) {
|
|
580
|
+
if (child === clearStack) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
hasOtherElements = true;
|
|
584
|
+
break;
|
|
594
585
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
let attribute = '';
|
|
599
|
-
if (section.substring(type.length, type.length + 1) == ' ') {
|
|
600
|
-
const colon = section.indexOf(':');
|
|
601
|
-
if (colon == -1) {
|
|
602
|
-
return '{' + section + '}';
|
|
603
|
-
}
|
|
604
|
-
options = section.substring(colon + 1);
|
|
605
|
-
attribute = section.substring(type.length + 1, colon);
|
|
606
|
-
}
|
|
607
|
-
else {
|
|
608
|
-
options = section.substring(type.length + 1);
|
|
609
|
-
}
|
|
610
|
-
// TODO: Check - previously there was no second parameter here
|
|
611
|
-
const rotation = rotate(options.replace(/"/g, '"').replace(/'/g, '''), null);
|
|
612
|
-
if (attribute) {
|
|
613
|
-
set(attribute, rotation[0]);
|
|
614
|
-
}
|
|
615
|
-
return '<a class="squiffy-link" data-' + type + '="' + rotation[1] + '" data-attribute="' + attribute + '" role="link">' + rotation[0] + '</a>';
|
|
586
|
+
|
|
587
|
+
if (!hasOtherElements) {
|
|
588
|
+
unClearScreen();
|
|
616
589
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
f();
|
|
626
|
-
},
|
|
627
|
-
};
|
|
628
|
-
|
|
629
|
-
function startsWith(string: string, prefix: string) {
|
|
630
|
-
return string.substring(0, prefix.length) === prefix;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
function rotate(options: string, current: string | null) {
|
|
634
|
-
const colon = options.indexOf(':');
|
|
635
|
-
if (colon == -1) {
|
|
636
|
-
return [options, current];
|
|
637
|
-
}
|
|
638
|
-
const next = options.substring(0, colon);
|
|
639
|
-
let remaining = options.substring(colon + 1);
|
|
640
|
-
if (current) remaining += ':' + current;
|
|
641
|
-
return [next, remaining];
|
|
590
|
+
|
|
591
|
+
setCurrentSectionElement();
|
|
592
|
+
setCurrentPassageElement();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (!canGoBack()) {
|
|
596
|
+
emitter.emit("canGoBackChanged", { canGoBack: false });
|
|
597
|
+
}
|
|
642
598
|
}
|
|
643
599
|
|
|
644
|
-
|
|
600
|
+
// We create a separate div inside the passed-in element. This allows us to clear the text output, but
|
|
601
|
+
// without affecting any overlays that may have been added to the container (for transitions).
|
|
602
|
+
const outputElementContainer = options.element;
|
|
603
|
+
const outputElement = document.createElement("div");
|
|
604
|
+
outputElementContainer.appendChild(outputElement);
|
|
645
605
|
story = options.story;
|
|
646
606
|
|
|
647
|
-
settings = {
|
|
648
|
-
scroll: options.scroll ||
|
|
607
|
+
const settings = {
|
|
608
|
+
scroll: options.scroll || "body",
|
|
649
609
|
persist: (options.persist === undefined) ? true : options.persist,
|
|
650
610
|
onSet: options.onSet || (() => {})
|
|
651
611
|
};
|
|
@@ -655,16 +615,85 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
|
|
|
655
615
|
settings.persist = false;
|
|
656
616
|
}
|
|
657
617
|
|
|
658
|
-
if (settings.scroll ===
|
|
659
|
-
outputElement.style.overflowY =
|
|
618
|
+
if (settings.scroll === "element") {
|
|
619
|
+
outputElement.style.overflowY = "auto";
|
|
660
620
|
}
|
|
661
621
|
|
|
662
|
-
|
|
663
|
-
|
|
622
|
+
outputElement.addEventListener("click", handleClick);
|
|
623
|
+
outputElement.addEventListener("keypress", async function (event) {
|
|
624
|
+
if (event.key !== "Enter") return;
|
|
625
|
+
await handleClick(event);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
let undoLog: Record<string, any> = {};
|
|
629
|
+
|
|
630
|
+
const onSet = function(attribute: string, oldValue: any) {
|
|
631
|
+
if (attribute == "_output") return;
|
|
632
|
+
if (attribute in undoLog) return;
|
|
633
|
+
undoLog[attribute] = oldValue;
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
const writeUndoLog = function() {
|
|
637
|
+
(currentPassageElement ?? currentSectionElement).setAttribute("data-undo", JSON.stringify(undoLog));
|
|
638
|
+
undoLog = {};
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
const doUndo = function(undosJson: string | null) {
|
|
642
|
+
if (!undosJson) return;
|
|
643
|
+
const undos = JSON.parse(undosJson) as Record<string, any>;
|
|
644
|
+
if (!undos) return;
|
|
645
|
+
for (const attribute of Object.keys(undos)) {
|
|
646
|
+
state.setInternal(attribute, undos[attribute], false);
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
const state = new State(settings.persist, story.id || "", settings.onSet, emitter, onSet);
|
|
651
|
+
const get = state.get.bind(state);
|
|
652
|
+
const set = state.set.bind(state);
|
|
653
|
+
|
|
654
|
+
const textProcessor = new TextProcessor(story, state, () => currentSection);
|
|
655
|
+
const linkHandler = new LinkHandler();
|
|
656
|
+
|
|
657
|
+
const getSectionText = (sectionName: string) => {
|
|
658
|
+
if (sectionName in story.sections) {
|
|
659
|
+
return story.sections[sectionName].text || null;
|
|
660
|
+
}
|
|
661
|
+
return null;
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
const getPassageText = (name: string) => {
|
|
665
|
+
if (currentSection.passages && name in currentSection.passages) {
|
|
666
|
+
return currentSection.passages[name].text || null;
|
|
667
|
+
} else if ("passages" in story.sections[""] && story.sections[""].passages && name in story.sections[""].passages) {
|
|
668
|
+
return story.sections[""].passages![name].text || null;
|
|
669
|
+
}
|
|
670
|
+
return null;
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
const addTransition = (fn: () => Promise<void>) => {
|
|
674
|
+
transitions.push(fn);
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
const animation = new Animation();
|
|
678
|
+
|
|
679
|
+
const pluginManager = new PluginManager(outputElement, textProcessor, state, linkHandler,
|
|
680
|
+
getSectionText, getPassageText, ui.processText, addTransition, animation, emitter);
|
|
681
|
+
pluginManager.add(Plugins.ReplaceLabel());
|
|
682
|
+
pluginManager.add(Plugins.RotateSequencePlugin());
|
|
683
|
+
pluginManager.add(Plugins.RandomPlugin());
|
|
684
|
+
pluginManager.add(Plugins.LivePlugin());
|
|
685
|
+
pluginManager.add(Plugins.AnimatePlugin());
|
|
664
686
|
|
|
665
687
|
return {
|
|
688
|
+
begin: begin,
|
|
666
689
|
restart: restart,
|
|
667
690
|
get: get,
|
|
668
691
|
set: set,
|
|
692
|
+
clickLink: handleLink,
|
|
693
|
+
update: update,
|
|
694
|
+
goBack: goBack,
|
|
695
|
+
on: (e, h) => emitter.on(e, h),
|
|
696
|
+
off: (e, h) => emitter.off(e, h),
|
|
697
|
+
once: (e, h) => emitter.once(e, h),
|
|
669
698
|
};
|
|
670
699
|
};
|