@gramlot/gramlot-examples 0.3.4 → 0.3.6

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.6.
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.6",
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"));
@@ -6838,33 +6879,160 @@ Please report this to https://github.com/markedjs/marked.`, e) {
6838
6879
  element.innerHTML = purifier.sanitize(markdown.parse(element.textContent), { USE_PROFILES: { html: true } });
6839
6880
  }
6840
6881
 
6882
+ // src/gramlot_examples/gallery/browser/gallery-content.js
6883
+ var pending = /* @__PURE__ */ new WeakMap();
6884
+ function mountGalleryContent(renderer, root) {
6885
+ const document2 = renderer.destination.ownerDocument;
6886
+ const byId = (id) => document2.getElementById(id);
6887
+ const node = (element) => renderer.elements.get(element).node;
6888
+ const entries2 = /* @__PURE__ */ new Map();
6889
+ let disposed = false;
6890
+ function format({ key, view }) {
6891
+ if (view === "description") {
6892
+ const readme = byId(`readme-${key}`);
6893
+ readme.textContent = node(readme).value;
6894
+ renderReadme(readme);
6895
+ return;
6896
+ }
6897
+ const code = byId(`code-${key}`);
6898
+ code.textContent = node(code).value;
6899
+ highlightCode(code);
6900
+ const logic = byId(`logic-${key}`);
6901
+ if (logic) {
6902
+ const source = logicSource(node(logic).value);
6903
+ node(byId(`logic-panel-${key}`)).setAttr({ hidden: source === null });
6904
+ logic.textContent = source ?? "";
6905
+ if (source !== null) highlightCode(logic);
6906
+ }
6907
+ }
6908
+ async function load(entry) {
6909
+ if (entry.loading) return;
6910
+ entry.loading = true;
6911
+ const targetId = entry.view === "code" ? `code-tabs-${entry.key}` : entry.element.id;
6912
+ const target = node(byId(targetId));
6913
+ const current = () => {
6914
+ const element = byId(targetId);
6915
+ return element && renderer.elements.get(element)?.node === target ? element : null;
6916
+ };
6917
+ let request = pending.get(target);
6918
+ if (!request) {
6919
+ request = renderer.binding.gramlot.src.remoteSource(target, "lesson", { key: entry.key, view: entry.view });
6920
+ pending.set(target, request);
6921
+ }
6922
+ try {
6923
+ const mounted = await request;
6924
+ if (disposed || !current() || !mounted) return;
6925
+ format(entry);
6926
+ observer.unobserve(entry.element);
6927
+ entries2.delete(entry.element);
6928
+ } catch {
6929
+ if (disposed || !current()) return;
6930
+ const status = current().querySelector('[role="status"]');
6931
+ node(status).setValue("Could not load this content. Close and reopen the panel to retry.");
6932
+ } finally {
6933
+ if (pending.get(target) === request) pending.delete(target);
6934
+ entry.loading = false;
6935
+ }
6936
+ }
6937
+ const observer = new document2.defaultView.IntersectionObserver((records) => {
6938
+ for (const record of records) {
6939
+ const entry = entries2.get(record.target);
6940
+ if (!entry) continue;
6941
+ const visible = record.isIntersecting && record.intersectionRect.width > 0 && record.intersectionRect.height > 0;
6942
+ if (visible && !entry.visible) void load(entry);
6943
+ entry.visible = visible;
6944
+ }
6945
+ });
6946
+ const dispose = () => {
6947
+ disposed = true;
6948
+ observer.disconnect();
6949
+ entries2.clear();
6950
+ };
6951
+ renderer.onDispose(root, dispose);
6952
+ return {
6953
+ observe(key) {
6954
+ for (const [view, id, loaded] of [
6955
+ ["code", `codepane-${key}`, `code-${key}`],
6956
+ ["description", `description-${key}`, `readme-${key}`]
6957
+ ]) {
6958
+ const element = byId(id);
6959
+ const entry = { key, view, element, visible: false, loading: false };
6960
+ if (byId(loaded)) format(entry);
6961
+ else if (!entries2.has(element)) {
6962
+ entries2.set(element, entry);
6963
+ observer.observe(element);
6964
+ }
6965
+ }
6966
+ },
6967
+ dispose
6968
+ };
6969
+ }
6970
+
6841
6971
  // src/gramlot_examples/gallery/browser/gallery.js
6842
6972
  function mountGallery(renderer, root, inspector) {
6843
6973
  const state = root.builder.data.getItem("galleryIndex");
6844
6974
  const settings = mountGallerySettings(renderer, root, state);
6845
- const guide = mountGuidedDemo(renderer, root);
6975
+ let guide = null;
6976
+ const content = mountGalleryContent(renderer, root);
6846
6977
  const mobile = mountGalleryMobile(renderer, root, inspector, state, (visible) => {
6847
- if (!visible) guide.pause();
6978
+ if (!visible) guide?.pause();
6848
6979
  });
6849
6980
  const index = mountIndexPage(renderer, root, (entries2, active) => {
6850
6981
  inspector.update(entries2, active);
6851
6982
  mobile.update(entries2, active);
6852
- guide.update(entries2, active);
6853
- }, (entry, { byId, node, set, listen }) => {
6983
+ guide?.update(entries2, active);
6984
+ }, (entry, { byId }) => {
6854
6985
  const { key } = entry;
6855
- renderReadme(byId(`readme-${key}`));
6856
- if (entry.panel.classList.contains("index-category")) return;
6857
- highlightCode(byId(`code-${key}`));
6858
- const logic = byId(`logic-${key}`);
6859
- if (logic) {
6860
- const source = logicSource(node(logic).value);
6861
- set(byId(`logic-panel-${key}`), { hidden: source === null });
6862
- logic.textContent = source ?? "";
6863
- if (source !== null) highlightCode(logic);
6986
+ if (key === "l04" && !guide) guide = mountGuidedDemo(renderer, root);
6987
+ if (entry.panel.classList.contains("index-category")) {
6988
+ renderReadme(byId(`readme-${key}`));
6989
+ return;
6864
6990
  }
6865
- }, { containerLayout: true, state });
6991
+ content.observe(entry.key);
6992
+ }, {
6993
+ containerLayout: true,
6994
+ state,
6995
+ async createEntry(entry) {
6996
+ const document2 = renderer.destination.ownerDocument;
6997
+ const node = (id) => renderer.elements.get(document2.getElementById(id)).node;
6998
+ const source = (id) => root.builder.wrapSource(node(id));
6999
+ const category = entry.open.classList.contains("index-family");
7000
+ const pane = source("index-pages").contentPane({
7001
+ id: `panel-${entry.key}`,
7002
+ title: entry.open.textContent,
7003
+ pageName: entry.key,
7004
+ hidden: true,
7005
+ class: category ? "index-category" : "gallery-panel"
7006
+ });
7007
+ const code = category ? null : source("gallery-code-pages").div({
7008
+ id: `codepane-${entry.key}`,
7009
+ class: "gallery-codepane",
7010
+ hidden: true,
7011
+ height: "100%"
7012
+ });
7013
+ if (code) code.tabContainer({
7014
+ id: `code-tabs-${entry.key}`,
7015
+ class: "gallery-source-tabs",
7016
+ height: "100%",
7017
+ tabMode: "underline"
7018
+ }).contentPane({ title: "Code" }).p("Loading code\u2026", { role: "status" });
7019
+ try {
7020
+ const runtime = renderer.binding.gramlot;
7021
+ const mounted = await runtime.src.remoteSource(node(`panel-${entry.key}`), "lesson", { key: entry.key });
7022
+ if (!mounted) throw new Error("Lesson loading was cancelled");
7023
+ } catch (error) {
7024
+ for (const target of [pane, code]) if (target?.parentBag) target.parentBag.popNode(target.label);
7025
+ throw error;
7026
+ }
7027
+ },
7028
+ onLoad(key, status) {
7029
+ const element = renderer.destination.ownerDocument.getElementById("gallery-load-status");
7030
+ renderer.elements.get(element).node.setValue(status === "loading" ? "Loading lesson\u2026" : status === "error" ? "Could not load this lesson. Select it again to retry." : "");
7031
+ }
7032
+ });
6866
7033
  return { dispose() {
6867
- guide.dispose();
7034
+ guide?.dispose();
7035
+ content.dispose();
6868
7036
  settings.dispose();
6869
7037
  index.dispose();
6870
7038
  mobile.dispose();
@@ -7470,11 +7638,11 @@ Please report this to https://github.com/markedjs/marked.`, e) {
7470
7638
  this.error = error.message;
7471
7639
  }
7472
7640
  if (message.type === "result") {
7473
- const pending = this.#pending.get(message.id);
7474
- if (pending) {
7641
+ const pending2 = this.#pending.get(message.id);
7642
+ if (pending2) {
7475
7643
  this.#pending.delete(message.id);
7476
- if (message.ok && !this.error) pending.resolve(message);
7477
- else pending.reject(new Error(message.error || this.error));
7644
+ if (message.ok && !this.error) pending2.resolve(message);
7645
+ else pending2.reject(new Error(message.error || this.error));
7478
7646
  }
7479
7647
  }
7480
7648
  this.#notify();
@@ -7531,7 +7699,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
7531
7699
  this.#port.removeEventListener("message", this.#receive);
7532
7700
  this.#port.close();
7533
7701
  this.state = "disposed";
7534
- for (const pending of this.#pending.values()) pending.reject(new Error("Inspected page disconnected"));
7702
+ for (const pending2 of this.#pending.values()) pending2.reject(new Error("Inspected page disconnected"));
7535
7703
  this.#pending.clear();
7536
7704
  this.#notify();
7537
7705
  }
@@ -7610,9 +7778,9 @@ Please report this to https://github.com/markedjs/marked.`, e) {
7610
7778
  _subscribe() {
7611
7779
  if (this._store) this._store.subscribe(this._subId, { any: () => {
7612
7780
  if (this._pendingRender) return;
7613
- const pending = this._pendingRender = {};
7781
+ const pending2 = this._pendingRender = {};
7614
7782
  queueMicrotask(() => {
7615
- if (this._pendingRender === pending && this.isConnected) this._render();
7783
+ if (this._pendingRender === pending2 && this.isConnected) this._render();
7616
7784
  });
7617
7785
  } });
7618
7786
  }
@@ -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,25 @@ 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 === 'description' && !item.examples) root.div(item.readme, {id: `readme-${key}`});
106
+ else if (view === 'example') {
107
+ if (item.examples) this.groupPanel(root, item);
108
+ else this.panel(root, item);
109
+ } else throw new Error(`Unknown gallery view: ${key}/${view}`);
110
+ }
101
111
  panel(parent, example) {
102
112
  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'});
113
+ const panel = parent.div({class: 'gallery-panel-layout'});
105
114
  const header = panel.header({class: 'gallery-explanation'});
106
115
  header.h2(example.title);
107
116
  header.p('In this example: ' + example.description);
@@ -116,13 +125,12 @@ export class GalleryPage extends IndexPage {
116
125
  guidedDemo(layout);
117
126
  }
118
127
  stage.iframe({title: `${example.title} preview`, name: `example-${key}`, id: `frame-${key}`});
119
- preview.contentPane({title: 'Explanation', pageName: 'explanation', class: 'gallery-lesson', overflow: 'auto'})
120
- .div(example.readme, {id: `readme-${key}`});
128
+ preview.contentPane({title: 'Explanation', pageName: 'explanation', class: 'gallery-lesson', overflow: 'auto', id: `description-${key}`})
129
+ .p('Loading explanation…', {role: 'status'});
121
130
  }
122
131
  codePanel(parent, example) {
123
132
  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'});
133
+ const codepane = parent;
126
134
  codepane.contentPane({title: 'JavaScript', pageName: 'javascript', id: `code-panel-${key}`})
127
135
  .pre({tabindex: 0}).code(example.source, {class: 'language-javascript', id: `code-${key}`});
128
136
  if (example.logic != null) {
@@ -131,3 +139,5 @@ export class GalleryPage extends IndexPage {
131
139
  }
132
140
  }
133
141
  }
142
+
143
+ 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
  });