@bobfrankston/rmfmail 1.0.650 → 1.0.652

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.
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ // Copy @bobfrankston/rmf-tiny's adapter.js into client/lib/ so the compose
3
+ // page can dynamic-import it at runtime. msger's custom protocol serves
4
+ // files only from contentDir (client/), and `node_modules/` lives at
5
+ // app/node_modules — outside that root. A build-time copy keeps the public
6
+ // adapter entry reachable without changing msger's served root or messing
7
+ // with junctions that npm install would clobber.
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+
11
+ const root = path.resolve(import.meta.dirname, "..");
12
+ const src = path.join(root, "node_modules", "@bobfrankston", "rmf-tiny", "src", "adapter.js");
13
+ const dst = path.join(root, "client", "lib", "rmf-tiny.js");
14
+
15
+ if (!fs.existsSync(src)) {
16
+ console.error(`build-rmf-tiny: source not found at ${src} — run npm install first`);
17
+ process.exit(1);
18
+ }
19
+ fs.copyFileSync(src, dst);
20
+ console.log(`build-rmf-tiny: copied ${path.relative(root, src)} → ${path.relative(root, dst)}`);
@@ -130,8 +130,28 @@ function setActiveEditorBadge(type: EditorTypeChoice | "fallback"): void {
130
130
  tinymce: "TinyMCE",
131
131
  fallback: "plain (fallback)",
132
132
  };
133
+ const docs: Record<string, string> = {
134
+ quill: "https://quilljs.com/docs/quickstart",
135
+ tiptap: "https://tiptap.dev/docs/editor/introduction",
136
+ tinymce: "https://www.tiny.cloud/docs/tinymce/6/",
137
+ fallback: "https://github.com/BobFrankston/mailx/blob/master/app/docs/editor.md",
138
+ };
133
139
  el.textContent = labels[type] || type;
134
140
  el.dataset.editor = type;
141
+ const url = docs[type];
142
+ if (url) {
143
+ el.style.cursor = "pointer";
144
+ el.title = `Open ${labels[type]} documentation`;
145
+ el.onclick = () => {
146
+ const api = (window as any).mailxapi;
147
+ if (api?.openExternal) api.openExternal(url);
148
+ else window.open(url, "_blank", "noopener,noreferrer");
149
+ };
150
+ } else {
151
+ el.style.cursor = "";
152
+ el.title = "";
153
+ el.onclick = null;
154
+ }
135
155
  }
136
156
 
137
157
  async function tryEditor(type: EditorTypeChoice): Promise<MailxEditor | null> {
@@ -651,7 +651,12 @@ async function createTinyMceEditor(container: HTMLElement): Promise<MailxEditor>
651
651
  cdnUrl = localStorage.getItem("mailx-tinymce-cdn") || DEFAULT_TINYMCE_CDN;
652
652
  apiKey = localStorage.getItem("mailx-tinymce-apikey") || undefined;
653
653
  } catch { /* private mode — use defaults */ }
654
- const m = (await import("@bobfrankston/rmf-tiny" as any)) as {
654
+ // Build step copies node_modules/@bobfrankston/rmf-tiny/src/adapter.js
655
+ // → client/lib/rmf-tiny.js. Bare-name `@bobfrankston/rmf-tiny` resolution
656
+ // would target `node_modules/`, which msger's custom protocol can't reach
657
+ // (outside contentDir=client/). The relative path stays inside the
658
+ // served root.
659
+ const m = (await import("../lib/rmf-tiny.js" as any)) as {
655
660
  createTinyMceEditor(container: HTMLElement, opts: { cdnUrl?: string; apiKey?: string }): Promise<MailxEditor>;
656
661
  };
657
662
  const ed = await m.createTinyMceEditor(container, { cdnUrl, apiKey });
@@ -0,0 +1,141 @@
1
+ /**
2
+ * @bobfrankston/rmf-tiny — TinyMCE adapter for rmfmail.
3
+ *
4
+ * MIT-licensed adapter glue. Imports TinyMCE at runtime; ships no
5
+ * TinyMCE bytes. The user is responsible for making TinyMCE reachable
6
+ * to their environment:
7
+ *
8
+ * - Desktop / Node-side install: `npm install tinymce`
9
+ * (rmfmail's createEditor("tinymce") branch dynamically imports
10
+ * this adapter, which dynamically imports tinymce from
11
+ * node_modules.)
12
+ *
13
+ * - Android / browser-only WebView: pass `cdnUrl` in the options to
14
+ * have the adapter inject a <script src=…> tag. The bytes are
15
+ * fetched directly from Tiny's CDN (or whatever URL the user
16
+ * configured); the host APK / page never carries them.
17
+ *
18
+ * The adapter implements the same `MailxEditor` shape as the Quill /
19
+ * tiptap factories in rmfmail's editor.ts, so the host code path is
20
+ * identical regardless of which editor is in use.
21
+ */
22
+ /** Load tinymce. Tries native module import first (desktop / npm-installed);
23
+ * falls back to script-tag injection from cdnUrl. Resolves to whatever's
24
+ * on `window.tinymce`. */
25
+ async function loadTinymce(opts) {
26
+ // 1. Try the npm-installed path. Wrapped in try because in browser-only
27
+ // environments the module specifier won't resolve and we want to
28
+ // fall through to CDN injection.
29
+ try {
30
+ const mod = await import(/* @vite-ignore */ "tinymce");
31
+ const tinymce = mod.default || mod;
32
+ // Pull in the plugins TinyMCE's paste-from-Word handler needs. These
33
+ // are side-effect imports — they register themselves on the global.
34
+ await Promise.all([
35
+ import("tinymce/themes/silver").catch(() => { }),
36
+ import("tinymce/icons/default").catch(() => { }),
37
+ import("tinymce/models/dom").catch(() => { }),
38
+ import("tinymce/plugins/paste").catch(() => { }),
39
+ import("tinymce/plugins/lists").catch(() => { }),
40
+ import("tinymce/plugins/link").catch(() => { }),
41
+ import("tinymce/plugins/table").catch(() => { }),
42
+ import("tinymce/plugins/code").catch(() => { }),
43
+ import("tinymce/plugins/image").catch(() => { }),
44
+ ]);
45
+ return tinymce;
46
+ }
47
+ catch {
48
+ // Fall through.
49
+ }
50
+ // 2. Already loaded by a prior call.
51
+ const w = window;
52
+ if (w.tinymce)
53
+ return w.tinymce;
54
+ // 3. Inject from CDN if provided.
55
+ if (!opts.cdnUrl) {
56
+ throw new Error("rmf-tiny: tinymce not installed (npm install tinymce) and no cdnUrl supplied. See README for Android setup.");
57
+ }
58
+ const url = opts.apiKey && !opts.cdnUrl.includes("api-key")
59
+ ? `${opts.cdnUrl}${opts.cdnUrl.includes("?") ? "&" : "?"}apiKey=${encodeURIComponent(opts.apiKey)}`
60
+ : opts.cdnUrl;
61
+ await new Promise((resolve, reject) => {
62
+ const s = document.createElement("script");
63
+ s.src = url;
64
+ s.referrerPolicy = "origin";
65
+ s.onload = () => resolve();
66
+ s.onerror = () => reject(new Error(`rmf-tiny: failed to load TinyMCE from ${url}`));
67
+ document.head.appendChild(s);
68
+ });
69
+ if (!w.tinymce)
70
+ throw new Error("rmf-tiny: TinyMCE script loaded but window.tinymce is missing");
71
+ return w.tinymce;
72
+ }
73
+ /** Create a TinyMCE-backed MailxEditor in `container`. */
74
+ export async function createTinyMceEditor(container, opts = {}) {
75
+ const tinymce = await loadTinymce(opts);
76
+ // TinyMCE attaches to a target element by id selector; create one
77
+ // inside the host container so multiple editors on a page don't
78
+ // collide and so we don't pollute the caller's element with TinyMCE
79
+ // attributes directly.
80
+ const target = document.createElement("div");
81
+ target.id = `rmf-tiny-${Math.random().toString(36).slice(2, 10)}`;
82
+ target.style.cssText = "width:100%;height:100%;";
83
+ container.appendChild(target);
84
+ const editor = await new Promise((resolve) => {
85
+ tinymce.init({
86
+ target,
87
+ // Word-paste fidelity is the entire point of using TinyMCE here.
88
+ // `paste_as_text: false` keeps formatting; powerpaste isn't in
89
+ // OSS but the standard paste plugin handles Word reasonably well.
90
+ plugins: "paste lists link table code image preview searchreplace",
91
+ toolbar: [
92
+ "undo redo | bold italic underline strikethrough | forecolor backcolor",
93
+ "bullist numlist outdent indent | link table image code",
94
+ ].join(" | "),
95
+ menubar: "file edit view insert format",
96
+ // Right-click menu — TinyMCE's built-in context menu plugin.
97
+ // Without this set, the WebView's default context menu fires
98
+ // (browser-level cut/copy/paste only). The string lists which
99
+ // contextmenu items show up; "link image table" are stock
100
+ // TinyMCE entries that pick up automatically when the user
101
+ // right-clicks on each kind of element.
102
+ contextmenu: "link image table | cut copy paste | undo redo",
103
+ statusbar: false,
104
+ branding: false,
105
+ license_key: "gpl",
106
+ paste_data_images: true,
107
+ // Permissive valid_elements — preserve as much of the source
108
+ // formatting as possible. The default is more aggressive about
109
+ // stripping. Empty string for `valid_elements` means accept
110
+ // everything that the schema allows.
111
+ paste_word_valid_elements: "@[style|class],-strong/b,-em/i,-u,-s,-sub,-sup,-strike,-p,-ol,-ul,-li,-h1,-h2,-h3,-h4,-h5,-h6,-blockquote,-table[border|cellpadding|cellspacing|width|height|class|style],-tr,-td[colspan|rowspan|width|height|class|style|valign|align|background|bgcolor],-th,-thead,-tbody,-tfoot,-pre,-br,-a[href|target|title],-img[src|alt|width|height|style|class]",
112
+ paste_retain_style_properties: "color background background-color font-family font-size font-weight font-style text-decoration text-align padding padding-top padding-bottom padding-left padding-right margin margin-top margin-bottom margin-left margin-right border border-top border-bottom border-left border-right",
113
+ content_style: "body { font-family: system-ui, sans-serif; font-size: 14px; }",
114
+ init_instance_callback: (ed) => resolve(ed),
115
+ setup: (ed) => {
116
+ if (opts.initialHtml)
117
+ ed.on("init", () => ed.setContent(opts.initialHtml));
118
+ },
119
+ });
120
+ });
121
+ return {
122
+ setHtml(html) { editor.setContent(html); },
123
+ getHtml() { return editor.getContent(); },
124
+ getText() { return editor.getContent({ format: "text" }); },
125
+ focus() { editor.focus(); },
126
+ setCursor(_pos) {
127
+ // TinyMCE addresses positions through ranges; rmfmail callers
128
+ // mostly use this for "put cursor at end" semantics. Approximate
129
+ // by selecting the end of body content.
130
+ try {
131
+ editor.selection.select(editor.getBody(), true);
132
+ editor.selection.collapse(false);
133
+ }
134
+ catch { /* */ }
135
+ },
136
+ get root() { return editor.getContainer(); },
137
+ on(event, handler) { editor.on(event, handler); },
138
+ off(event, handler) { editor.off(event, handler); },
139
+ };
140
+ }
141
+ //# sourceMappingURL=adapter.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/rmfmail",
3
- "version": "1.0.650",
3
+ "version": "1.0.652",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -23,7 +23,7 @@
23
23
  "client"
24
24
  ],
25
25
  "scripts": {
26
- "build": "tsc -p bin && node bin/build-icon-ico.js && node bin/build-rmfmailto-exe.js",
26
+ "build": "tsc -p bin && node bin/build-icon-ico.js && node bin/build-rmfmailto-exe.js && node bin/build-rmf-tiny.js",
27
27
  "watch": "tsc -w",
28
28
  "start": "node --watch packages/mailx-server/index.js",
29
29
  "start:prod": "node packages/mailx-server/index.js",