@gramlot/gramlot-examples 0.3.4 → 0.3.5

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/README.md CHANGED
@@ -12,7 +12,7 @@ npm install @gramlot/gramlot-examples
12
12
  [![PyPI](https://img.shields.io/pypi/v/gramlot-examples)](https://pypi.org/project/gramlot-examples/)
13
13
  [![npm](https://img.shields.io/npm/v/@gramlot/gramlot-examples)](https://www.npmjs.com/package/@gramlot/gramlot-examples)
14
14
 
15
- Current release: 0.3.4.
15
+ Current release: 0.3.5.
16
16
 
17
17
  [Documentation and learning sequence](https://gramlot-examples.readthedocs.io/en/latest/)
18
18
  include every lesson, its Python and JavaScript source, and the gallery guide.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gramlot/gramlot-examples",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Gramlot example pages, the gallery page and its catalogue.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -14,7 +14,7 @@
14
14
  "prepack": "node scripts/build-browser.mjs"
15
15
  },
16
16
  "dependencies": {
17
- "@gramlot/gramlot": ">=0.3.2"
17
+ "@gramlot/gramlot": ">=0.3.3"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@genrojs/bag": ">=0.11.0",
@@ -1920,7 +1920,9 @@
1920
1920
  // node_modules/@gramlot/gramlot/src/index-page-runtime.js
1921
1921
  function mountIndexPage(renderer, root, onSelection = () => {
1922
1922
  }, prepareEntry = () => {
1923
- }, { containerLayout = false, state: suppliedState = null } = {}) {
1923
+ }, { containerLayout = false, state: suppliedState = null, createEntry = null, onLoad = () => {
1924
+ } } = {}) {
1925
+ if (createEntry && !containerLayout) throw new TypeError("Deferred index entries require containerLayout");
1924
1926
  const shell = renderer.records.get(root).element;
1925
1927
  const document2 = shell.ownerDocument;
1926
1928
  const window2 = document2.defaultView;
@@ -2028,17 +2030,53 @@
2028
2030
  set(shell, { data_code_visible: String(visible) });
2029
2031
  }
2030
2032
  };
2033
+ const prepare = (entry) => {
2034
+ entry.panel = byId(`panel-${entry.key}`);
2035
+ entry.tab = containerLayout ? entry.panel : byId(`tab-${entry.key}`);
2036
+ entry.code = byId(`codepane-${entry.key}`);
2037
+ if (entry.key === "intro") return;
2038
+ prepareEntry(entry, { byId, node, set, listen });
2039
+ entry.frame = byId(`frame-${entry.key}`);
2040
+ if (entry.frame) listen(entry.frame, "load", () => send(entry.frame));
2041
+ };
2042
+ const openEntry = (entry) => {
2043
+ if (opened.getItem(entry.key)) return;
2044
+ opened.setItem(entry.key, true);
2045
+ set(containerLayout ? entry.panel : entry.tab, { hidden: false });
2046
+ if (entry.frame) set(entry.frame, { src: entry.open.getAttribute("href") });
2047
+ };
2031
2048
  let selecting = false;
2032
2049
  function select(key) {
2033
- if (selecting) return;
2050
+ if (selecting || disposed) return;
2051
+ const requested = entries2.get(key);
2052
+ if (!requested) return;
2053
+ state.setItem("requested", key);
2054
+ if (!requested.panel) {
2055
+ onLoad(key, "loading");
2056
+ if (!requested.loading) {
2057
+ requested.loading = Promise.resolve().then(() => {
2058
+ if (!disposed) return createEntry(requested);
2059
+ }).then(() => {
2060
+ if (disposed) return;
2061
+ prepare(requested);
2062
+ openEntry(requested);
2063
+ if (state.getItem("requested") === key) {
2064
+ onLoad(key, "ready");
2065
+ select(key);
2066
+ }
2067
+ }).catch((error) => {
2068
+ if (!disposed && state.getItem("requested") === key) onLoad(key, "error", error);
2069
+ }).finally(() => {
2070
+ requested.loading = null;
2071
+ });
2072
+ }
2073
+ return;
2074
+ }
2075
+ onLoad(key, "ready");
2034
2076
  selecting = true;
2035
2077
  try {
2036
2078
  const entry = entries2.get(key);
2037
- if (!opened.getItem(key)) {
2038
- opened.setItem(key, true);
2039
- set(containerLayout ? entry.panel : entry.tab, { hidden: false });
2040
- if (entry.frame) set(entry.frame, { src: entry.open.getAttribute("href") });
2041
- }
2079
+ openEntry(entry);
2042
2080
  if (entry.family) set(entry.family, { open: true });
2043
2081
  for (const [current, item] of entries2) {
2044
2082
  const active = current === key;
@@ -2202,9 +2240,9 @@
2202
2240
  });
2203
2241
  listen(window2, "resize", () => setSidebarWidth(state.getItem("preferredSidebarWidth")));
2204
2242
  }
2205
- const choices = containerLayout ? byId("index-pages").children : shell.querySelectorAll(".index-tabs button");
2243
+ const choices = createEntry ? [byId("panel-intro"), ...shell.querySelectorAll('.index-sidebar a[id^="open-"]')] : containerLayout ? byId("index-pages").children : shell.querySelectorAll(".index-tabs button");
2206
2244
  for (const tab of choices) {
2207
- const key = tab.id.slice(containerLayout ? "panel-".length : "tab-".length);
2245
+ const key = tab.id.slice(createEntry && tab.id.startsWith("open-") ? "open-".length : containerLayout ? "panel-".length : "tab-".length);
2208
2246
  const entry = {
2209
2247
  key,
2210
2248
  tab,
@@ -2213,7 +2251,7 @@
2213
2251
  open: key === "intro" ? null : byId(`open-${key}`)
2214
2252
  };
2215
2253
  entries2.set(key, entry);
2216
- if (containerLayout ? node(tab).getAttr("hidden") !== true : !tab.hidden) opened.setItem(key, true);
2254
+ if (entry.panel && (containerLayout ? node(entry.panel).getAttr("hidden") !== true : !tab.hidden)) opened.setItem(key, true);
2217
2255
  if (!containerLayout && tab.getAttribute("aria-selected") === "true") state.setItem("active", key);
2218
2256
  if (!containerLayout) listen(tab, "click", () => select(key));
2219
2257
  if (entry.open) listen(entry.open, "click", (event) => {
@@ -2231,12 +2269,15 @@
2231
2269
  });
2232
2270
  if (key === "intro") continue;
2233
2271
  entry.family = entry.open.closest("details");
2234
- prepareEntry(entry, { byId, node, set, listen });
2235
- if (entry.panel.classList.contains("index-category")) continue;
2236
- entry.frame = byId(`frame-${key}`);
2237
- listen(entry.frame, "load", () => send(entry.frame));
2272
+ if (entry.panel) prepare(entry);
2238
2273
  }
2239
2274
  if (containerLayout) {
2275
+ const pages = byId("index-pages");
2276
+ listen(pages, "click", (event) => {
2277
+ if (event.target === pages && event.composedPath().some((element) => element instanceof window2.Element && element.getAttribute("role") === "tab")) {
2278
+ select(state.getItem("active"));
2279
+ }
2280
+ });
2240
2281
  const subscription = `${shell.id}-selection`;
2241
2282
  state.subscribe(subscription, { any: (event) => {
2242
2283
  if (event.node === state.getNode("active")) select(state.getItem("active"));
@@ -6842,16 +6883,17 @@ Please report this to https://github.com/markedjs/marked.`, e) {
6842
6883
  function mountGallery(renderer, root, inspector) {
6843
6884
  const state = root.builder.data.getItem("galleryIndex");
6844
6885
  const settings = mountGallerySettings(renderer, root, state);
6845
- const guide = mountGuidedDemo(renderer, root);
6886
+ let guide = null;
6846
6887
  const mobile = mountGalleryMobile(renderer, root, inspector, state, (visible) => {
6847
- if (!visible) guide.pause();
6888
+ if (!visible) guide?.pause();
6848
6889
  });
6849
6890
  const index = mountIndexPage(renderer, root, (entries2, active) => {
6850
6891
  inspector.update(entries2, active);
6851
6892
  mobile.update(entries2, active);
6852
- guide.update(entries2, active);
6893
+ guide?.update(entries2, active);
6853
6894
  }, (entry, { byId, node, set, listen }) => {
6854
6895
  const { key } = entry;
6896
+ if (key === "l04" && !guide) guide = mountGuidedDemo(renderer, root);
6855
6897
  renderReadme(byId(`readme-${key}`));
6856
6898
  if (entry.panel.classList.contains("index-category")) return;
6857
6899
  highlightCode(byId(`code-${key}`));
@@ -6862,9 +6904,47 @@ Please report this to https://github.com/markedjs/marked.`, e) {
6862
6904
  logic.textContent = source ?? "";
6863
6905
  if (source !== null) highlightCode(logic);
6864
6906
  }
6865
- }, { containerLayout: true, state });
6907
+ }, {
6908
+ containerLayout: true,
6909
+ state,
6910
+ async createEntry(entry) {
6911
+ const document2 = renderer.destination.ownerDocument;
6912
+ const node = (id) => renderer.elements.get(document2.getElementById(id)).node;
6913
+ const source = (id) => root.builder.wrapSource(node(id));
6914
+ const category = entry.open.classList.contains("index-family");
6915
+ const pane = source("index-pages").contentPane({
6916
+ id: `panel-${entry.key}`,
6917
+ title: entry.open.textContent,
6918
+ pageName: entry.key,
6919
+ hidden: true,
6920
+ class: category ? "index-category" : "gallery-panel"
6921
+ });
6922
+ const code = category ? null : source("gallery-code-pages").tabContainer({
6923
+ id: `codepane-${entry.key}`,
6924
+ class: "gallery-codepane gallery-source-tabs",
6925
+ hidden: true,
6926
+ height: "100%",
6927
+ tabMode: "underline"
6928
+ });
6929
+ try {
6930
+ const runtime = renderer.binding.gramlot;
6931
+ const results = await Promise.all([
6932
+ runtime.src.remoteSource(node(`panel-${entry.key}`), "lesson", { key: entry.key }),
6933
+ ...code ? [runtime.src.remoteSource(node(`codepane-${entry.key}`), "lesson", { key: entry.key, view: "code" })] : []
6934
+ ]);
6935
+ if (results.some((mounted) => !mounted)) throw new Error("Lesson loading was cancelled");
6936
+ } catch (error) {
6937
+ for (const target of [pane, code]) if (target?.parentBag) target.parentBag.popNode(target.label);
6938
+ throw error;
6939
+ }
6940
+ },
6941
+ onLoad(key, status) {
6942
+ const element = renderer.destination.ownerDocument.getElementById("gallery-load-status");
6943
+ renderer.elements.get(element).node.setValue(status === "loading" ? "Loading lesson\u2026" : status === "error" ? "Could not load this lesson. Select it again to retry." : "");
6944
+ }
6945
+ });
6866
6946
  return { dispose() {
6867
- guide.dispose();
6947
+ guide?.dispose();
6868
6948
  settings.dispose();
6869
6949
  index.dispose();
6870
6950
  mobile.dispose();
@@ -10,8 +10,8 @@ export class GalleryPage extends IndexPage {
10
10
  static galleryScript = '/gallery/dist/gallery.js';
11
11
  static css = [...IndexPage.css, '/gallery/gallery.css'];
12
12
  static get navigation() {
13
- return this.families.map(group => ({...group,
14
- children: group.examples.map(item => ({...item, url: item.frameUrl}))}));
13
+ return this.families.map(group => ({key: group.key, title: group.title,
14
+ children: group.examples.map(item => ({key: item.key, title: item.title, url: item.frameUrl}))}));
15
15
  }
16
16
  main(root) {
17
17
  const families = this.constructor.navigation;
@@ -38,17 +38,10 @@ export class GalleryPage extends IndexPage {
38
38
  const panels = center.tabContainer({id: 'index-pages', class: 'index-main', height: '100%',
39
39
  selectedPage: '^galleryIndex.active', tabMode: 'tabs'});
40
40
  this.introduction(panels.contentPane({id: 'panel-intro', title: 'Introduction', pageName: 'intro', class: 'index-intro'}));
41
- for (const family of families) {
42
- this.groupPanel(panels.contentPane({id: `panel-${family.key}`, title: family.title,
43
- pageName: family.key, class: 'index-category', hidden: true}), family);
44
- for (const example of family.children) this.panel(panels, example);
45
- }
46
41
  const code = regions.contentPane({region: 'right', width: '420px', minSize: 220, maxSize: 900,
47
42
  splitter: true, drawer: true, drawer_icon: '⌘', drawer_label: 'Code', drawer_minimize: true,
48
43
  node_id: 'galleryCode', id: 'index-code', aria_label: 'Example source code', hidden: true});
49
- for (const family of families) {
50
- for (const example of family.children) this.codePanel(code, example);
51
- }
44
+ code.div({id: 'gallery-code-pages', height: '100%'});
52
45
  this.footer(shell.footer({class: 'index-footer'}));
53
46
  this.behavior(shell);
54
47
  }
@@ -84,6 +77,7 @@ export class GalleryPage extends IndexPage {
84
77
  header.html_button('☰', {type: 'button', id: 'gallery-mobile-menu', class: 'gallery-mobile-only',
85
78
  aria_label: 'Navigation', aria_controls: 'index-sidebar', aria_expanded: 'false'});
86
79
  header.h1(this.constructor.title);
80
+ header.span('', {id: 'gallery-load-status', role: 'status', aria_live: 'polite'});
87
81
  header.html_button('Code ⟨/⟩', {type: 'button', id: 'index-code-toggle',
88
82
  aria_controls: 'index-code', aria_expanded: 'false', disabled: true});
89
83
  const views = header.div({class: 'gallery-mobile-views gallery-mobile-only', role: 'group', aria_label: 'Gallery view'});
@@ -98,10 +92,24 @@ export class GalleryPage extends IndexPage {
98
92
  shell.script({src: this.constructor.galleryScript});
99
93
  }
100
94
 
95
+ galleryItem(key) {
96
+ for (const family of this.constructor.families) {
97
+ if (family.key === key) return family;
98
+ for (const example of family.examples) if (example.key === key) return example;
99
+ }
100
+ throw new Error(`Unknown gallery entry: ${key}`);
101
+ }
102
+ lesson(root, {key, view = 'example'}) {
103
+ const item = this.galleryItem(key);
104
+ if (view === 'code' && !item.examples) this.codePanel(root, item);
105
+ else if (view === 'example') {
106
+ if (item.examples) this.groupPanel(root, item);
107
+ else this.panel(root, item);
108
+ } else throw new Error(`Unknown gallery view: ${key}/${view}`);
109
+ }
101
110
  panel(parent, example) {
102
111
  const key = example.key;
103
- const panel = parent.contentPane({id: `panel-${key}`, title: example.title, pageName: key,
104
- class: 'gallery-panel', hidden: true}).div({class: 'gallery-panel-layout'});
112
+ const panel = parent.div({class: 'gallery-panel-layout'});
105
113
  const header = panel.header({class: 'gallery-explanation'});
106
114
  header.h2(example.title);
107
115
  header.p('In this example: ' + example.description);
@@ -121,8 +129,7 @@ export class GalleryPage extends IndexPage {
121
129
  }
122
130
  codePanel(parent, example) {
123
131
  const key = example.key;
124
- const codepane = parent.tabContainer({class: 'gallery-codepane gallery-source-tabs', id: `codepane-${key}`,
125
- hidden: true, height: '100%', tabMode: 'underline'});
132
+ const codepane = parent;
126
133
  codepane.contentPane({title: 'JavaScript', pageName: 'javascript', id: `code-panel-${key}`})
127
134
  .pre({tabindex: 0}).code(example.source, {class: 'language-javascript', id: `code-${key}`});
128
135
  if (example.logic != null) {
@@ -131,3 +138,5 @@ export class GalleryPage extends IndexPage {
131
138
  }
132
139
  }
133
140
  }
141
+
142
+ GalleryPage.registerSource('lesson');
@@ -14,14 +14,14 @@ export class Page extends GalleryPage {
14
14
  return families.map(({key, title, path, examples}) => {
15
15
  const read = name => readFileSync(join(path, name), 'utf8');
16
16
  return {
17
- key, title, readme: read('README.md'),
17
+ key, title, get readme() { return read('README.md'); },
18
18
  examples: examples.map(example => ({
19
19
  key: example.key, title: example.title, description: example.description, folder: example.folder, frameUrl: example.key,
20
- readme: read(`${example.folder}.md`),
21
- source: read(`${example.folder}.js`),
20
+ get readme() { return read(`${example.folder}.md`); },
21
+ get source() { return read(`${example.folder}.js`); },
22
22
  // The page module shows its own Logic; only a companion NN_name_aux.js is shown apart.
23
- logic: routes[example.key].logic === routes[example.key].page
24
- ? null : readFileSync(routes[example.key].logic, 'utf8'),
23
+ get logic() { return routes[example.key].logic === routes[example.key].page
24
+ ? null : readFileSync(routes[example.key].logic, 'utf8'); },
25
25
  })),
26
26
  };
27
27
  });