squiffy-runtime 6.0.0-alpha.7 → 6.0.0-alpha.9

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.
@@ -14,7 +14,7 @@ export interface SquiffyApi {
14
14
  restart: () => void;
15
15
  get: (attribute: string) => any;
16
16
  set: (attribute: string, value: any) => void;
17
- clickLink: (link: HTMLElement) => void;
17
+ clickLink: (link: HTMLElement) => boolean;
18
18
  update: (story: Story) => void;
19
19
  }
20
20
  interface SquiffyJsFunctionApi {
@@ -31,8 +31,11 @@ export const init = (options) => {
31
31
  return JSON.parse(result);
32
32
  }
33
33
  function handleLink(link) {
34
+ const outputSection = link.closest('.squiffy-output-section');
35
+ if (outputSection !== currentSectionElement)
36
+ return false;
34
37
  if (link.classList.contains('disabled'))
35
- return;
38
+ return false;
36
39
  let passage = link.getAttribute('data-passage');
37
40
  let section = link.getAttribute('data-section');
38
41
  const rotateAttr = link.getAttribute('data-rotate');
@@ -55,15 +58,16 @@ export const init = (options) => {
55
58
  showPassage('@last');
56
59
  }
57
60
  }
61
+ return true;
58
62
  }
59
- else if (section) {
60
- disableLink(link);
63
+ if (section) {
61
64
  section = processLink(section);
62
65
  if (section) {
63
66
  go(section);
64
67
  }
68
+ return true;
65
69
  }
66
- else if (rotateOrSequenceAttr) {
70
+ if (rotateOrSequenceAttr) {
67
71
  const result = rotate(rotateOrSequenceAttr, rotateAttr ? link.innerText : '');
68
72
  link.innerHTML = result[0].replace(/"/g, '"').replace(/'/g, '\'');
69
73
  const dataAttribute = rotateAttr ? 'data-rotate' : 'data-sequence';
@@ -76,7 +80,9 @@ export const init = (options) => {
76
80
  set(attribute, result[0]);
77
81
  }
78
82
  save();
83
+ return true;
79
84
  }
85
+ return false;
80
86
  }
81
87
  function handleClick(event) {
82
88
  const target = event.target;
@@ -88,9 +94,6 @@ export const init = (options) => {
88
94
  link.classList.add('disabled');
89
95
  link.setAttribute('tabindex', '-1');
90
96
  }
91
- function disableLinks(links) {
92
- links.forEach(disableLink);
93
- }
94
97
  function begin() {
95
98
  if (!load()) {
96
99
  go(story.start);
@@ -336,7 +339,6 @@ export const init = (options) => {
336
339
  }
337
340
  function newSection(sectionName) {
338
341
  if (currentSectionElement) {
339
- disableLinks(currentSectionElement.querySelectorAll('.squiffy-link'));
340
342
  currentSectionElement.querySelectorAll('input').forEach(el => {
341
343
  const attribute = el.getAttribute('data-attribute') || el.id;
342
344
  if (attribute)
@@ -589,8 +591,20 @@ export const init = (options) => {
589
591
  function getPassageContent(section, passage) {
590
592
  return outputElement.querySelectorAll(`[data-source='[[${section}]][${passage}]']`);
591
593
  }
594
+ function updateElementTextPreservingDisabledPassageLinks(element, text) {
595
+ // Record which passage links are disabled
596
+ const disabledPassages = Array.from(element
597
+ .querySelectorAll("a.link-passage.disabled"))
598
+ .map((el) => el.getAttribute("data-passage"));
599
+ element.innerHTML = text;
600
+ // Re-disable links that were disabled before the update
601
+ for (const passage of disabledPassages) {
602
+ const link = element.querySelector(`a.link-passage[data-passage="${passage}"]`);
603
+ if (link)
604
+ disableLink(link);
605
+ }
606
+ }
592
607
  function update(newStory) {
593
- // TODO: Re-disable clicked links after update
594
608
  for (const existingSection of Object.keys(story.sections)) {
595
609
  const elements = getSectionContent(existingSection);
596
610
  if (elements.length) {
@@ -598,13 +612,14 @@ export const init = (options) => {
598
612
  if (!newSection) {
599
613
  // section has been deleted
600
614
  for (const element of elements) {
601
- element.remove();
615
+ const parentOutputSection = element.closest('.squiffy-output-section');
616
+ parentOutputSection.remove();
602
617
  }
603
618
  }
604
619
  else if (newSection.text && newSection.text != story.sections[existingSection].text) {
605
620
  // section has been updated
606
621
  for (const element of elements) {
607
- element.innerHTML = ui.processText(newSection.text);
622
+ updateElementTextPreservingDisabledPassageLinks(element, newSection.text);
608
623
  }
609
624
  }
610
625
  }
@@ -618,13 +633,14 @@ export const init = (options) => {
618
633
  if (!newPassage) {
619
634
  // passage has been deleted
620
635
  for (const element of elements) {
621
- element.remove();
636
+ const parentOutputBlock = element.closest('.squiffy-output-block');
637
+ parentOutputBlock.remove();
622
638
  }
623
639
  }
624
640
  else if (newPassage.text && newPassage.text != story.sections[existingSection].passages[existingPassage].text) {
625
641
  // passage has been updated
626
642
  for (const element of elements) {
627
- element.innerHTML = ui.processText(newPassage.text);
643
+ updateElementTextPreservingDisabledPassageLinks(element, newPassage.text);
628
644
  }
629
645
  }
630
646
  }
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "squiffy-runtime",
3
- "version": "6.0.0-alpha.7",
3
+ "version": "6.0.0-alpha.9",
4
4
  "main": "dist/squiffy.runtime.js",
5
5
  "types": "dist/squiffy.runtime.d.ts",
6
6
  "scripts": {
7
+ "dev": "vitest",
7
8
  "test": "vitest --run",
8
9
  "build": "tsc",
9
10
  "prepublishOnly": "npm run build && npm run test"
@@ -45,12 +45,12 @@ exports[`Click a section link 2`] = `
45
45
  "
46
46
  <div class="squiffy-output-section" id="squiffy-section-1" data-section="Introduction"><div class="squiffy-output-block"><div data-source="[[Introduction]]"><h1>Squiffy Test</h1>
47
47
  <p>This is <a href="http://textadventures.co.uk">a website link</a>.</p>
48
- <p>This should be <a class="squiffy-link link-passage disabled" data-passage="passage" role="link" tabindex="-1">a link to a passage</a>. Here's <a class="squiffy-link link-passage disabled" data-passage="passage2" role="link" tabindex="-1">another one</a>.</p>
49
- <p>You don't need to specify a name - for example, this <a class="squiffy-link link-passage disabled" data-passage="link" role="link" tabindex="-1">link</a> and this <a class="squiffy-link link-section disabled" data-section="section" role="link" tabindex="-1">section</a>.</p>
50
- <p>And this goes to the <a class="squiffy-link link-section disabled" data-section="section2" role="link" tabindex="-1">next section</a>.</p>
51
- <p>This line has links to <a class="squiffy-link link-section disabled" data-section="section 3" role="link" tabindex="-1">section 3</a> and <a class="squiffy-link link-section disabled" data-section="section four" role="link" tabindex="-1">section 4</a>.</p>
52
- <p>This line has links to <a class="squiffy-link link-passage disabled" data-passage="passage 3" role="link" tabindex="-1">passage 3</a> and <a class="squiffy-link link-passage disabled" data-passage="passage four" role="link" tabindex="-1">passage 4</a>.</p>
53
- <p>Oh look - <a class="squiffy-link link-passage disabled" data-passage="it's a passage with an apostrophe" role="link" tabindex="-1">it's a passage with an apostrophe</a>.</p></div></div></div><div class="squiffy-output-section" id="squiffy-section-2" data-section="section 3"><div class="squiffy-output-block"><div data-source="[[section 3]]"><p>Another section is here.</p></div></div></div>"
48
+ <p>This should be <a class="squiffy-link link-passage" data-passage="passage" role="link" tabindex="0">a link to a passage</a>. Here's <a class="squiffy-link link-passage" data-passage="passage2" role="link" tabindex="0">another one</a>.</p>
49
+ <p>You don't need to specify a name - for example, this <a class="squiffy-link link-passage" data-passage="link" role="link" tabindex="0">link</a> and this <a class="squiffy-link link-section" data-section="section" role="link" tabindex="0">section</a>.</p>
50
+ <p>And this goes to the <a class="squiffy-link link-section" data-section="section2" role="link" tabindex="0">next section</a>.</p>
51
+ <p>This line has links to <a class="squiffy-link link-section" data-section="section 3" role="link" tabindex="0">section 3</a> and <a class="squiffy-link link-section" data-section="section four" role="link" tabindex="0">section 4</a>.</p>
52
+ <p>This line has links to <a class="squiffy-link link-passage" data-passage="passage 3" role="link" tabindex="0">passage 3</a> and <a class="squiffy-link link-passage" data-passage="passage four" role="link" tabindex="0">passage 4</a>.</p>
53
+ <p>Oh look - <a class="squiffy-link link-passage" data-passage="it's a passage with an apostrophe" role="link" tabindex="0">it's a passage with an apostrophe</a>.</p></div></div></div><div class="squiffy-output-section" id="squiffy-section-2" data-section="section 3"><div class="squiffy-output-block"><div data-source="[[section 3]]"><p>Another section is here, with passages <a class="squiffy-link link-passage" data-passage="a" role="link" tabindex="0">a</a> and <a class="squiffy-link link-passage" data-passage="b" role="link" tabindex="0">b</a>.</p></div></div></div>"
54
54
  `;
55
55
 
56
56
  exports[`Delete passage 1`] = `
@@ -60,17 +60,17 @@ exports[`Delete passage 1`] = `
60
60
 
61
61
  exports[`Delete passage 2`] = `
62
62
  "
63
- <div class="squiffy-output-section" id="squiffy-section-1" data-section="_default"><div class="squiffy-output-block"><div data-source="[[_default]]"><p>Click this: <a class="squiffy-link link-passage disabled" data-passage="a" role="link" tabindex="-1">a</a></p></div></div><div class="squiffy-output-block"></div></div>"
63
+ <div class="squiffy-output-section" id="squiffy-section-1" data-section="_default"><div class="squiffy-output-block"><div data-source="[[_default]]"><p>Click this: <a class="squiffy-link link-passage disabled" data-passage="a" role="link" tabindex="-1">a</a></p></div></div></div>"
64
64
  `;
65
65
 
66
66
  exports[`Delete section 1`] = `
67
67
  "
68
- <div class="squiffy-output-section" id="squiffy-section-1" data-section="_default"><div class="squiffy-output-block"><div data-source="[[_default]]"><p>Click this: <a class="squiffy-link link-section disabled" data-section="a" role="link" tabindex="-1">a</a></p></div></div></div><div class="squiffy-output-section" id="squiffy-section-2" data-section="a"><div class="squiffy-output-block"><div data-source="[[a]]"><p>New section</p></div></div></div>"
68
+ <div class="squiffy-output-section" id="squiffy-section-1" data-section="_default"><div class="squiffy-output-block"><div data-source="[[_default]]"><p>Click this: <a class="squiffy-link link-section" data-section="a" role="link" tabindex="0">a</a></p></div></div></div><div class="squiffy-output-section" id="squiffy-section-2" data-section="a"><div class="squiffy-output-block"><div data-source="[[a]]"><p>New section</p></div></div></div>"
69
69
  `;
70
70
 
71
71
  exports[`Delete section 2`] = `
72
72
  "
73
- <div class="squiffy-output-section" id="squiffy-section-1" data-section="_default"><div class="squiffy-output-block"><div data-source="[[_default]]"><p>Click this: <a class="squiffy-link link-section disabled" data-section="a" role="link" tabindex="-1">a</a></p></div></div></div><div class="squiffy-output-section" id="squiffy-section-2" data-section="a"><div class="squiffy-output-block"></div></div>"
73
+ <div class="squiffy-output-section" id="squiffy-section-1" data-section="_default"><div class="squiffy-output-block"><div data-source="[[_default]]"><p>Click this: <a class="squiffy-link link-section" data-section="a" role="link" tabindex="0">a</a></p></div></div></div>"
74
74
  `;
75
75
 
76
76
  exports[`Update default section output 1`] = `
@@ -61,7 +61,9 @@ const initScript = async (script: string) => {
61
61
  };
62
62
 
63
63
  const findLink = (element: HTMLElement, linkType: string, linkText: string, onlyEnabled: boolean = false) => {
64
- const links = element.querySelectorAll(`a.squiffy-link.link-${linkType}`);
64
+ const links = onlyEnabled
65
+ ? element.querySelectorAll(`.squiffy-output-section:last-child a.squiffy-link.link-${linkType}`)
66
+ : element.querySelectorAll(`a.squiffy-link.link-${linkType}`);
65
67
  return Array.from(links).find(link => link.textContent === linkText && (onlyEnabled ? !link.classList.contains("disabled") : true)) as HTMLElement;
66
68
  };
67
69
 
@@ -100,13 +102,14 @@ test('Click a section link', async () => {
100
102
  expect(linkToPassage).not.toBeNull();
101
103
  expect(section3Link).not.toBeNull();
102
104
  expect(linkToPassage.classList).not.toContain('disabled');
103
- expect(section3Link.classList).not.toContain('disabled');
104
105
 
105
- squiffyApi.clickLink(section3Link);
106
+ const handled = squiffyApi.clickLink(section3Link);
107
+ expect(handled).toBe(true);
106
108
 
107
- expect(linkToPassage.classList).toContain('disabled');
108
- expect(section3Link.classList).toContain('disabled');
109
109
  expect(element.innerHTML).toMatchSnapshot();
110
+
111
+ // passage link is from the previous section, so should be unclickable
112
+ expect(squiffyApi.clickLink(linkToPassage)).toBe(false);
110
113
  });
111
114
 
112
115
  test('Click a passage link', async () => {
@@ -121,13 +124,15 @@ test('Click a passage link', async () => {
121
124
  expect(linkToPassage).not.toBeNull();
122
125
  expect(section3Link).not.toBeNull();
123
126
  expect(linkToPassage.classList).not.toContain('disabled');
124
- expect(section3Link.classList).not.toContain('disabled');
125
127
 
126
- squiffyApi.clickLink(linkToPassage);
128
+ const handled = squiffyApi.clickLink(linkToPassage);
129
+ expect(handled).toBe(true);
127
130
 
128
131
  expect(linkToPassage.classList).toContain('disabled');
129
- expect(section3Link.classList).not.toContain('disabled');
130
132
  expect(element.innerHTML).toMatchSnapshot();
133
+
134
+ // shouldn't be able to click it again
135
+ expect(squiffyApi.clickLink(linkToPassage)).toBe(false);
131
136
  });
132
137
 
133
138
  test('Run JavaScript functions', async () => {
@@ -156,7 +161,8 @@ test('Run JavaScript functions', async () => {
156
161
  const clickContinue = () => {
157
162
  const continueLink = findLink(element, 'section', 'Continue...', true);
158
163
  expect(continueLink).not.toBeNull();
159
- squiffyApi.clickLink(continueLink);
164
+ const handled = squiffyApi.clickLink(continueLink);
165
+ expect(handled).toBe(true);
160
166
  };
161
167
 
162
168
  const { squiffyApi, element } = await initScript(script);
@@ -208,7 +214,8 @@ test('Update passage output', async () => {
208
214
  Passage a content`);
209
215
 
210
216
  const link = findLink(element, 'passage', 'a');
211
- squiffyApi.clickLink(link);
217
+ const handled = squiffyApi.clickLink(link);
218
+ expect(handled).toBe(true);
212
219
 
213
220
  let defaultOutput = getSectionContent(element, '_default');
214
221
  expect(defaultOutput).toBe('Click this: a');
@@ -238,7 +245,8 @@ test('Delete section', async () => {
238
245
  New section`);
239
246
 
240
247
  const link = findLink(element, 'section', 'a');
241
- squiffyApi.clickLink(link);
248
+ const handled = squiffyApi.clickLink(link);
249
+ expect(handled).toBe(true);
242
250
 
243
251
  let defaultOutput = getSectionContent(element, '_default');
244
252
  expect(defaultOutput).toBe('Click this: a');
@@ -265,7 +273,8 @@ test('Delete passage', async () => {
265
273
  New passage`);
266
274
 
267
275
  const link = findLink(element, 'passage', 'a');
268
- squiffyApi.clickLink(link);
276
+ const handled = squiffyApi.clickLink(link);
277
+ expect(handled).toBe(true);
269
278
 
270
279
  let defaultOutput = getSectionContent(element, '_default');
271
280
  expect(defaultOutput).toBe('Click this: a');
@@ -283,4 +292,42 @@ New passage`);
283
292
  expect(passageOutput).toBeNull();
284
293
 
285
294
  expect(element.innerHTML).toMatchSnapshot();
295
+ });
296
+
297
+ test('Clicked passage links remain disabled after an update', async () => {
298
+ const { squiffyApi, element } = await initScript(`Click one of these: [a] [b]
299
+
300
+ [a]:
301
+ Output for passage A.
302
+
303
+ [b]:
304
+ Output for passage B.`);
305
+
306
+ // click linkA
307
+
308
+ let linkA = findLink(element, 'passage', 'a');
309
+ expect(linkA.classList).not.toContain('disabled');
310
+ expect(squiffyApi.clickLink(linkA)).toBe(true);
311
+
312
+ const updated = await compile(`Click one of these (updated): [a] [b]
313
+
314
+ [a]:
315
+ Output for passage A.
316
+
317
+ [b]:
318
+ Output for passage B.`);
319
+
320
+ squiffyApi.update(updated.story);
321
+
322
+ // linkA should still be disabled
323
+
324
+ linkA = findLink(element, 'passage', 'a');
325
+ expect(linkA.classList).toContain('disabled');
326
+ expect(squiffyApi.clickLink(linkA)).toBe(false);
327
+
328
+ // linkB should still be enabled
329
+
330
+ let linkB = findLink(element, 'passage', 'b');
331
+ expect(linkB.classList).not.toContain('disabled');
332
+ expect(squiffyApi.clickLink(linkB)).toBe(true);
286
333
  });
@@ -16,7 +16,7 @@ export interface SquiffyApi {
16
16
  restart: () => void;
17
17
  get: (attribute: string) => any;
18
18
  set: (attribute: string, value: any) => void;
19
- clickLink: (link: HTMLElement) => void;
19
+ clickLink: (link: HTMLElement) => boolean;
20
20
  update: (story: Story) => void;
21
21
  }
22
22
 
@@ -93,8 +93,12 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
93
93
  return JSON.parse(result);
94
94
  }
95
95
 
96
- function handleLink(link: HTMLElement) {
97
- if (link.classList.contains('disabled')) return;
96
+ function handleLink(link: HTMLElement): boolean {
97
+ const outputSection = link.closest('.squiffy-output-section');
98
+ if (outputSection !== currentSectionElement) return false;
99
+
100
+ if (link.classList.contains('disabled')) return false;
101
+
98
102
  let passage = link.getAttribute('data-passage');
99
103
  let section = link.getAttribute('data-section');
100
104
  const rotateAttr = link.getAttribute('data-rotate');
@@ -117,15 +121,20 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
117
121
  showPassage('@last');
118
122
  }
119
123
  }
124
+
125
+ return true;
120
126
  }
121
- else if (section) {
122
- disableLink(link);
127
+
128
+ if (section) {
123
129
  section = processLink(section);
124
130
  if (section) {
125
131
  go(section);
126
132
  }
133
+
134
+ return true;
127
135
  }
128
- else if (rotateOrSequenceAttr) {
136
+
137
+ if (rotateOrSequenceAttr) {
129
138
  const result = rotate(rotateOrSequenceAttr, rotateAttr ? link.innerText : '');
130
139
  link.innerHTML = result[0]!.replace(/&quot;/g, '"').replace(/&#39;/g, '\'');
131
140
  const dataAttribute = rotateAttr ? 'data-rotate' : 'data-sequence';
@@ -138,7 +147,11 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
138
147
  set(attribute, result[0]);
139
148
  }
140
149
  save();
150
+
151
+ return true;
141
152
  }
153
+
154
+ return false;
142
155
  }
143
156
 
144
157
  function handleClick(event: Event) {
@@ -153,10 +166,6 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
153
166
  link.setAttribute('tabindex', '-1');
154
167
  }
155
168
 
156
- function disableLinks(links: NodeListOf<Element>) {
157
- links.forEach(disableLink);
158
- }
159
-
160
169
  function begin() {
161
170
  if (!load()) {
162
171
  go(story.start);
@@ -412,7 +421,6 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
412
421
 
413
422
  function newSection(sectionName: string | null) {
414
423
  if (currentSectionElement) {
415
- disableLinks(currentSectionElement.querySelectorAll('.squiffy-link'));
416
424
  currentSectionElement.querySelectorAll('input').forEach(el => {
417
425
  const attribute = el.getAttribute('data-attribute') || el.id;
418
426
  if (attribute) set(attribute, el.value);
@@ -683,9 +691,22 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
683
691
  return outputElement.querySelectorAll(`[data-source='[[${section}]][${passage}]']`);
684
692
  }
685
693
 
686
- function update(newStory: Story) {
687
- // TODO: Re-disable clicked links after update
694
+ function updateElementTextPreservingDisabledPassageLinks(element: Element, text: string) {
695
+ // Record which passage links are disabled
696
+ const disabledPassages = Array.from(element
697
+ .querySelectorAll("a.link-passage.disabled"))
698
+ .map((el: HTMLElement) => el.getAttribute("data-passage"));
688
699
 
700
+ element.innerHTML = text;
701
+
702
+ // Re-disable links that were disabled before the update
703
+ for (const passage of disabledPassages) {
704
+ const link = element.querySelector(`a.link-passage[data-passage="${passage}"]`);
705
+ if (link) disableLink(link);
706
+ }
707
+ }
708
+
709
+ function update(newStory: Story) {
689
710
  for (const existingSection of Object.keys(story.sections)) {
690
711
  const elements = getSectionContent(existingSection);
691
712
  if (elements.length) {
@@ -693,13 +714,14 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
693
714
  if (!newSection) {
694
715
  // section has been deleted
695
716
  for (const element of elements) {
696
- element.remove();
717
+ const parentOutputSection = element.closest('.squiffy-output-section');
718
+ parentOutputSection.remove();
697
719
  }
698
720
  }
699
721
  else if (newSection.text && newSection.text != story.sections[existingSection].text) {
700
722
  // section has been updated
701
723
  for (const element of elements) {
702
- element.innerHTML = ui.processText(newSection.text);
724
+ updateElementTextPreservingDisabledPassageLinks(element, newSection.text);
703
725
  }
704
726
  }
705
727
  }
@@ -714,13 +736,14 @@ export const init = (options: SquiffyInitOptions): SquiffyApi => {
714
736
  if (!newPassage) {
715
737
  // passage has been deleted
716
738
  for (const element of elements) {
717
- element.remove();
739
+ const parentOutputBlock = element.closest('.squiffy-output-block');
740
+ parentOutputBlock.remove();
718
741
  }
719
742
  }
720
743
  else if (newPassage.text && newPassage.text != story.sections[existingSection].passages[existingPassage].text) {
721
744
  // passage has been updated
722
745
  for (const element of elements) {
723
- element.innerHTML = ui.processText(newPassage.text);
746
+ updateElementTextPreservingDisabledPassageLinks(element, newPassage.text);
724
747
  }
725
748
  }
726
749
  }