@bobfrankston/rmfmail 1.0.467
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 +429 -0
- package/bin/build-icon-ico.js +283 -0
- package/bin/lean-accounts.js +172 -0
- package/bin/mailx.js +1580 -0
- package/bin/mailx.js.map +1 -0
- package/bin/mailx.ts +1549 -0
- package/bin/postinstall.js +39 -0
- package/bin/postinstall.js.map +1 -0
- package/bin/postinstall.ts +41 -0
- package/bin/tsconfig.json +10 -0
- package/client/.gitattributes +10 -0
- package/client/.msger-window.json +1 -0
- package/client/android.html +226 -0
- package/client/app.js +3400 -0
- package/client/app.js.map +1 -0
- package/client/app.ts +3190 -0
- package/client/components/address-book.js +199 -0
- package/client/components/address-book.js.map +1 -0
- package/client/components/address-book.ts +204 -0
- package/client/components/alarms.js +299 -0
- package/client/components/alarms.js.map +1 -0
- package/client/components/alarms.ts +276 -0
- package/client/components/calendar-sidebar.js +508 -0
- package/client/components/calendar-sidebar.js.map +1 -0
- package/client/components/calendar-sidebar.ts +474 -0
- package/client/components/calendar.js +217 -0
- package/client/components/calendar.js.map +1 -0
- package/client/components/calendar.ts +211 -0
- package/client/components/context-menu.js +80 -0
- package/client/components/context-menu.js.map +1 -0
- package/client/components/context-menu.ts +95 -0
- package/client/components/folder-picker.js +119 -0
- package/client/components/folder-picker.js.map +1 -0
- package/client/components/folder-picker.ts +127 -0
- package/client/components/folder-tree.js +1093 -0
- package/client/components/folder-tree.js.map +1 -0
- package/client/components/folder-tree.ts +1069 -0
- package/client/components/message-list.js +1140 -0
- package/client/components/message-list.js.map +1 -0
- package/client/components/message-list.ts +1129 -0
- package/client/components/message-viewer.js +1291 -0
- package/client/components/message-viewer.js.map +1 -0
- package/client/components/message-viewer.ts +1257 -0
- package/client/components/outbox-view.js +104 -0
- package/client/components/outbox-view.js.map +1 -0
- package/client/components/outbox-view.ts +102 -0
- package/client/components/tasks.js +261 -0
- package/client/components/tasks.js.map +1 -0
- package/client/components/tasks.ts +234 -0
- package/client/compose/compose.css +560 -0
- package/client/compose/compose.html +52 -0
- package/client/compose/compose.js +1289 -0
- package/client/compose/compose.js.map +1 -0
- package/client/compose/compose.ts +1231 -0
- package/client/compose/editor.js +616 -0
- package/client/compose/editor.js.map +1 -0
- package/client/compose/editor.ts +599 -0
- package/client/compose/ghost-text.js +122 -0
- package/client/compose/ghost-text.js.map +1 -0
- package/client/compose/ghost-text.ts +140 -0
- package/client/favicon.svg +11 -0
- package/client/icon.ico +0 -0
- package/client/icon.png +0 -0
- package/client/index.html +230 -0
- package/client/lib/android-bootstrap.js +9 -0
- package/client/lib/android-bootstrap.js.map +1 -0
- package/client/lib/android-bootstrap.ts +9 -0
- package/client/lib/api-client.js +378 -0
- package/client/lib/api-client.js.map +1 -0
- package/client/lib/api-client.ts +439 -0
- package/client/lib/local-service.js +603 -0
- package/client/lib/local-service.js.map +1 -0
- package/client/lib/local-service.ts +646 -0
- package/client/lib/local-store.js +214 -0
- package/client/lib/local-store.js.map +1 -0
- package/client/lib/local-store.ts +283 -0
- package/client/lib/mailxapi.js +304 -0
- package/client/lib/message-state.js +132 -0
- package/client/lib/message-state.js.map +1 -0
- package/client/lib/message-state.ts +160 -0
- package/client/package.json +16 -0
- package/client/styles/components.css +2049 -0
- package/client/styles/layout.css +332 -0
- package/client/styles/variables.css +85 -0
- package/client/tsconfig.json +19 -0
- package/package.json +124 -0
- package/packages/mailx-api/.gitattributes +10 -0
- package/packages/mailx-api/index.d.ts +10 -0
- package/packages/mailx-api/index.d.ts.map +1 -0
- package/packages/mailx-api/index.js +322 -0
- package/packages/mailx-api/index.js.map +1 -0
- package/packages/mailx-api/index.ts +283 -0
- package/packages/mailx-api/package.json +27 -0
- package/packages/mailx-api/tsconfig.json +9 -0
- package/packages/mailx-api/tsconfig.tsbuildinfo +1 -0
- package/packages/mailx-compose/.gitattributes +10 -0
- package/packages/mailx-compose/index.d.ts +21 -0
- package/packages/mailx-compose/index.d.ts.map +1 -0
- package/packages/mailx-compose/index.js +61 -0
- package/packages/mailx-compose/index.js.map +1 -0
- package/packages/mailx-compose/index.ts +85 -0
- package/packages/mailx-compose/package.json +18 -0
- package/packages/mailx-compose/tsconfig.json +9 -0
- package/packages/mailx-core/index.d.ts +139 -0
- package/packages/mailx-core/index.d.ts.map +1 -0
- package/packages/mailx-core/index.js +403 -0
- package/packages/mailx-core/index.js.map +1 -0
- package/packages/mailx-core/index.ts +424 -0
- package/packages/mailx-core/ipc.d.ts +13 -0
- package/packages/mailx-core/ipc.d.ts.map +1 -0
- package/packages/mailx-core/ipc.js +56 -0
- package/packages/mailx-core/ipc.js.map +1 -0
- package/packages/mailx-core/ipc.ts +62 -0
- package/packages/mailx-core/package.json +18 -0
- package/packages/mailx-core/tsconfig.json +9 -0
- package/packages/mailx-host/.gitattributes +10 -0
- package/packages/mailx-host/index.d.ts +21 -0
- package/packages/mailx-host/index.d.ts.map +1 -0
- package/packages/mailx-host/index.js +29 -0
- package/packages/mailx-host/index.js.map +1 -0
- package/packages/mailx-host/index.ts +38 -0
- package/packages/mailx-host/package.json +23 -0
- package/packages/mailx-host/tsconfig.json +9 -0
- package/packages/mailx-host/types-shim.d.ts +14 -0
- package/packages/mailx-imap/.gitattributes +10 -0
- package/packages/mailx-imap/index.d.ts +442 -0
- package/packages/mailx-imap/index.d.ts.map +1 -0
- package/packages/mailx-imap/index.js +3684 -0
- package/packages/mailx-imap/index.js.map +1 -0
- package/packages/mailx-imap/index.ts +3652 -0
- package/packages/mailx-imap/package-lock.json +131 -0
- package/packages/mailx-imap/package.json +28 -0
- package/packages/mailx-imap/providers/gmail-api.d.ts +8 -0
- package/packages/mailx-imap/providers/gmail-api.d.ts.map +1 -0
- package/packages/mailx-imap/providers/gmail-api.js +8 -0
- package/packages/mailx-imap/providers/gmail-api.js.map +1 -0
- package/packages/mailx-imap/providers/gmail-api.ts +8 -0
- package/packages/mailx-imap/providers/outlook-api.ts +7 -0
- package/packages/mailx-imap/providers/types.d.ts +9 -0
- package/packages/mailx-imap/providers/types.d.ts.map +1 -0
- package/packages/mailx-imap/providers/types.js +9 -0
- package/packages/mailx-imap/providers/types.js.map +1 -0
- package/packages/mailx-imap/providers/types.ts +9 -0
- package/packages/mailx-imap/tsconfig.json +9 -0
- package/packages/mailx-imap/tsconfig.tsbuildinfo +1 -0
- package/packages/mailx-send/.gitattributes +10 -0
- package/packages/mailx-send/README.md +217 -0
- package/packages/mailx-send/cli-queue.d.ts +11 -0
- package/packages/mailx-send/cli-queue.d.ts.map +1 -0
- package/packages/mailx-send/cli-queue.js +59 -0
- package/packages/mailx-send/cli-queue.js.map +1 -0
- package/packages/mailx-send/cli-queue.ts +62 -0
- package/packages/mailx-send/cli-send.d.ts +17 -0
- package/packages/mailx-send/cli-send.d.ts.map +1 -0
- package/packages/mailx-send/cli-send.js +75 -0
- package/packages/mailx-send/cli-send.js.map +1 -0
- package/packages/mailx-send/cli-send.ts +83 -0
- package/packages/mailx-send/cli.d.ts +22 -0
- package/packages/mailx-send/cli.d.ts.map +1 -0
- package/packages/mailx-send/cli.js +115 -0
- package/packages/mailx-send/cli.js.map +1 -0
- package/packages/mailx-send/cli.ts +126 -0
- package/packages/mailx-send/index.d.ts +77 -0
- package/packages/mailx-send/index.d.ts.map +1 -0
- package/packages/mailx-send/index.js +264 -0
- package/packages/mailx-send/index.js.map +1 -0
- package/packages/mailx-send/index.ts +333 -0
- package/packages/mailx-send/mailsend/README.md +133 -0
- package/packages/mailx-send/mailsend/cli.d.ts +12 -0
- package/packages/mailx-send/mailsend/cli.d.ts.map +1 -0
- package/packages/mailx-send/mailsend/cli.js +70 -0
- package/packages/mailx-send/mailsend/cli.js.map +1 -0
- package/packages/mailx-send/mailsend/cli.ts +81 -0
- package/packages/mailx-send/mailsend/index.d.ts +77 -0
- package/packages/mailx-send/mailsend/index.d.ts.map +1 -0
- package/packages/mailx-send/mailsend/index.js +264 -0
- package/packages/mailx-send/mailsend/index.js.map +1 -0
- package/packages/mailx-send/mailsend/index.ts +333 -0
- package/packages/mailx-send/mailsend/package-lock.json +65 -0
- package/packages/mailx-send/mailsend/package.json +26 -0
- package/packages/mailx-send/mailsend/tsconfig.json +21 -0
- package/packages/mailx-send/package-lock.json +65 -0
- package/packages/mailx-send/package.json +37 -0
- package/packages/mailx-send/tsconfig.json +21 -0
- package/packages/mailx-server/.gitattributes +10 -0
- package/packages/mailx-server/index.d.ts +8 -0
- package/packages/mailx-server/index.d.ts.map +1 -0
- package/packages/mailx-server/index.js +397 -0
- package/packages/mailx-server/index.js.map +1 -0
- package/packages/mailx-server/index.ts +429 -0
- package/packages/mailx-server/package.json +28 -0
- package/packages/mailx-server/tsconfig.json +9 -0
- package/packages/mailx-server/tsconfig.tsbuildinfo +1 -0
- package/packages/mailx-service/google-sync.d.ts +96 -0
- package/packages/mailx-service/google-sync.d.ts.map +1 -0
- package/packages/mailx-service/google-sync.js +153 -0
- package/packages/mailx-service/google-sync.js.map +1 -0
- package/packages/mailx-service/google-sync.ts +238 -0
- package/packages/mailx-service/index.d.ts +366 -0
- package/packages/mailx-service/index.d.ts.map +1 -0
- package/packages/mailx-service/index.js +2589 -0
- package/packages/mailx-service/index.js.map +1 -0
- package/packages/mailx-service/index.ts +2512 -0
- package/packages/mailx-service/jsonrpc.d.ts +25 -0
- package/packages/mailx-service/jsonrpc.d.ts.map +1 -0
- package/packages/mailx-service/jsonrpc.js +238 -0
- package/packages/mailx-service/jsonrpc.js.map +1 -0
- package/packages/mailx-service/jsonrpc.ts +268 -0
- package/packages/mailx-service/package.json +22 -0
- package/packages/mailx-service/tsconfig.json +9 -0
- package/packages/mailx-settings/.gitattributes +10 -0
- package/packages/mailx-settings/cloud.d.ts +47 -0
- package/packages/mailx-settings/cloud.d.ts.map +1 -0
- package/packages/mailx-settings/cloud.js +408 -0
- package/packages/mailx-settings/cloud.js.map +1 -0
- package/packages/mailx-settings/cloud.ts +393 -0
- package/packages/mailx-settings/index.d.ts +140 -0
- package/packages/mailx-settings/index.d.ts.map +1 -0
- package/packages/mailx-settings/index.js +872 -0
- package/packages/mailx-settings/index.js.map +1 -0
- package/packages/mailx-settings/index.ts +908 -0
- package/packages/mailx-settings/package.json +20 -0
- package/packages/mailx-settings/tsconfig.json +9 -0
- package/packages/mailx-settings/tsconfig.tsbuildinfo +1 -0
- package/packages/mailx-store/.gitattributes +10 -0
- package/packages/mailx-store/db.d.ts +346 -0
- package/packages/mailx-store/db.d.ts.map +1 -0
- package/packages/mailx-store/db.js +1796 -0
- package/packages/mailx-store/db.js.map +1 -0
- package/packages/mailx-store/db.ts +2007 -0
- package/packages/mailx-store/file-store.d.ts +36 -0
- package/packages/mailx-store/file-store.d.ts.map +1 -0
- package/packages/mailx-store/file-store.js +80 -0
- package/packages/mailx-store/file-store.js.map +1 -0
- package/packages/mailx-store/file-store.ts +82 -0
- package/packages/mailx-store/index.d.ts +7 -0
- package/packages/mailx-store/index.d.ts.map +1 -0
- package/packages/mailx-store/index.js +7 -0
- package/packages/mailx-store/index.js.map +1 -0
- package/packages/mailx-store/index.ts +7 -0
- package/packages/mailx-store/package.json +19 -0
- package/packages/mailx-store/tsconfig.json +9 -0
- package/packages/mailx-store/tsconfig.tsbuildinfo +1 -0
- package/packages/mailx-store-web/android-bootstrap.d.ts +16 -0
- package/packages/mailx-store-web/android-bootstrap.d.ts.map +1 -0
- package/packages/mailx-store-web/android-bootstrap.js +1247 -0
- package/packages/mailx-store-web/android-bootstrap.js.map +1 -0
- package/packages/mailx-store-web/android-bootstrap.ts +1262 -0
- package/packages/mailx-store-web/db.d.ts +132 -0
- package/packages/mailx-store-web/db.d.ts.map +1 -0
- package/packages/mailx-store-web/db.js +642 -0
- package/packages/mailx-store-web/db.js.map +1 -0
- package/packages/mailx-store-web/db.ts +756 -0
- package/packages/mailx-store-web/gmail-api-web.d.ts +11 -0
- package/packages/mailx-store-web/gmail-api-web.d.ts.map +1 -0
- package/packages/mailx-store-web/gmail-api-web.js +11 -0
- package/packages/mailx-store-web/gmail-api-web.js.map +1 -0
- package/packages/mailx-store-web/gmail-api-web.ts +11 -0
- package/packages/mailx-store-web/imap-web-provider.d.ts +33 -0
- package/packages/mailx-store-web/imap-web-provider.d.ts.map +1 -0
- package/packages/mailx-store-web/imap-web-provider.js +140 -0
- package/packages/mailx-store-web/imap-web-provider.js.map +1 -0
- package/packages/mailx-store-web/imap-web-provider.ts +156 -0
- package/packages/mailx-store-web/index.d.ts +10 -0
- package/packages/mailx-store-web/index.d.ts.map +1 -0
- package/packages/mailx-store-web/index.js +10 -0
- package/packages/mailx-store-web/index.js.map +1 -0
- package/packages/mailx-store-web/index.ts +10 -0
- package/packages/mailx-store-web/main-thread-host.d.ts +15 -0
- package/packages/mailx-store-web/main-thread-host.d.ts.map +1 -0
- package/packages/mailx-store-web/main-thread-host.js +292 -0
- package/packages/mailx-store-web/main-thread-host.js.map +1 -0
- package/packages/mailx-store-web/main-thread-host.ts +322 -0
- package/packages/mailx-store-web/package.json +23 -0
- package/packages/mailx-store-web/provider-types.d.ts +7 -0
- package/packages/mailx-store-web/provider-types.d.ts.map +1 -0
- package/packages/mailx-store-web/provider-types.js +7 -0
- package/packages/mailx-store-web/provider-types.js.map +1 -0
- package/packages/mailx-store-web/provider-types.ts +7 -0
- package/packages/mailx-store-web/sql-wasm-esm.js +10 -0
- package/packages/mailx-store-web/sql.js.d.ts +29 -0
- package/packages/mailx-store-web/sync-manager.d.ts +68 -0
- package/packages/mailx-store-web/sync-manager.d.ts.map +1 -0
- package/packages/mailx-store-web/sync-manager.js +506 -0
- package/packages/mailx-store-web/sync-manager.js.map +1 -0
- package/packages/mailx-store-web/sync-manager.ts +508 -0
- package/packages/mailx-store-web/tsconfig.json +10 -0
- package/packages/mailx-store-web/web-jsonrpc.d.ts +20 -0
- package/packages/mailx-store-web/web-jsonrpc.d.ts.map +1 -0
- package/packages/mailx-store-web/web-jsonrpc.js +102 -0
- package/packages/mailx-store-web/web-jsonrpc.js.map +1 -0
- package/packages/mailx-store-web/web-jsonrpc.ts +116 -0
- package/packages/mailx-store-web/web-message-store.d.ts +16 -0
- package/packages/mailx-store-web/web-message-store.d.ts.map +1 -0
- package/packages/mailx-store-web/web-message-store.js +89 -0
- package/packages/mailx-store-web/web-message-store.js.map +1 -0
- package/packages/mailx-store-web/web-message-store.ts +97 -0
- package/packages/mailx-store-web/web-service.d.ts +111 -0
- package/packages/mailx-store-web/web-service.d.ts.map +1 -0
- package/packages/mailx-store-web/web-service.js +542 -0
- package/packages/mailx-store-web/web-service.js.map +1 -0
- package/packages/mailx-store-web/web-service.ts +616 -0
- package/packages/mailx-store-web/web-settings.d.ts +83 -0
- package/packages/mailx-store-web/web-settings.d.ts.map +1 -0
- package/packages/mailx-store-web/web-settings.js +491 -0
- package/packages/mailx-store-web/web-settings.js.map +1 -0
- package/packages/mailx-store-web/web-settings.ts +522 -0
- package/packages/mailx-store-web/worker-bundle.js +6838 -0
- package/packages/mailx-store-web/worker-entry.d.ts +8 -0
- package/packages/mailx-store-web/worker-entry.d.ts.map +1 -0
- package/packages/mailx-store-web/worker-entry.js +187 -0
- package/packages/mailx-store-web/worker-entry.js.map +1 -0
- package/packages/mailx-store-web/worker-entry.ts +215 -0
- package/packages/mailx-store-web/worker-tcp-transport.d.ts +28 -0
- package/packages/mailx-store-web/worker-tcp-transport.d.ts.map +1 -0
- package/packages/mailx-store-web/worker-tcp-transport.js +98 -0
- package/packages/mailx-store-web/worker-tcp-transport.js.map +1 -0
- package/packages/mailx-store-web/worker-tcp-transport.ts +101 -0
- package/packages/mailx-types/.gitattributes +10 -0
- package/packages/mailx-types/index.d.ts +319 -0
- package/packages/mailx-types/index.d.ts.map +1 -0
- package/packages/mailx-types/index.js +243 -0
- package/packages/mailx-types/index.js.map +1 -0
- package/packages/mailx-types/index.ts +498 -0
- package/packages/mailx-types/package.json +16 -0
- package/packages/mailx-types/tsconfig.json +9 -0
- package/packages/mailx-types/tsconfig.tsbuildinfo +1 -0
- package/todo.json +13 -0
- package/tsconfig.base.json +31 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor abstraction — wraps Quill or tiptap behind a common interface.
|
|
3
|
+
* The compose window loads this module and calls createEditor() based on the user's setting.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface MailxEditor {
|
|
7
|
+
setHtml(html: string): void;
|
|
8
|
+
getHtml(): string;
|
|
9
|
+
getText(): string;
|
|
10
|
+
focus(): void;
|
|
11
|
+
setCursor(pos: number): void;
|
|
12
|
+
/** The editor's root editable element (for checking content) */
|
|
13
|
+
root: HTMLElement;
|
|
14
|
+
/** The scrollable container for positioning ghost text */
|
|
15
|
+
getScrollContainer(): HTMLElement;
|
|
16
|
+
/** Register a handler for content changes */
|
|
17
|
+
onContentChange(handler: () => void): void;
|
|
18
|
+
/** Register a keydown handler on the editor */
|
|
19
|
+
onKeyDown(handler: (e: KeyboardEvent) => void): void;
|
|
20
|
+
/** Insert plain text at the current cursor position */
|
|
21
|
+
insertTextAtCursor(text: string): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── Quill ──
|
|
25
|
+
|
|
26
|
+
declare const Quill: any;
|
|
27
|
+
|
|
28
|
+
/** URL-ish test: accepts http(s)://, mailto:, tel:, and bare domains with a dot. */
|
|
29
|
+
function looksLikeUrl(s: string): boolean {
|
|
30
|
+
const t = s.trim();
|
|
31
|
+
if (!t) return false;
|
|
32
|
+
if (/^(https?|mailto|tel):/i.test(t)) return true;
|
|
33
|
+
// bare domain (e.g. "example.com/path") — require a dot and no internal whitespace
|
|
34
|
+
return /^[\w-]+(\.[\w-]+)+(\/\S*)?$/.test(t);
|
|
35
|
+
}
|
|
36
|
+
function normalizeUrl(s: string): string {
|
|
37
|
+
const t = s.trim();
|
|
38
|
+
if (!t) return t;
|
|
39
|
+
if (/^(https?|mailto|tel):/i.test(t)) return t;
|
|
40
|
+
if (/^[\w.+-]+@[\w-]+(\.[\w-]+)+$/.test(t)) return `mailto:${t}`;
|
|
41
|
+
return `https://${t}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Floating modal that edits both link text and URL. Returns null on Cancel,
|
|
45
|
+
* { text, url } on OK, or { text: "", url: "" } on "Remove link". */
|
|
46
|
+
function openLinkDialog(initialText: string, initialUrl: string): Promise<{ text: string; url: string; remove?: boolean } | null> {
|
|
47
|
+
return new Promise(resolve => {
|
|
48
|
+
const backdrop = document.createElement("div");
|
|
49
|
+
backdrop.className = "mailx-modal-backdrop";
|
|
50
|
+
const panel = document.createElement("div");
|
|
51
|
+
panel.className = "mailx-modal";
|
|
52
|
+
panel.innerHTML = `
|
|
53
|
+
<div class="mailx-modal-title">Edit link</div>
|
|
54
|
+
<label class="mailx-modal-label">Text
|
|
55
|
+
<input type="text" class="mailx-modal-input" id="mailx-link-text">
|
|
56
|
+
</label>
|
|
57
|
+
<label class="mailx-modal-label">URL
|
|
58
|
+
<input type="text" class="mailx-modal-input" id="mailx-link-url" spellcheck="false" autocomplete="off">
|
|
59
|
+
</label>
|
|
60
|
+
<div class="mailx-modal-buttons">
|
|
61
|
+
<button type="button" class="mailx-modal-btn" data-action="remove">Remove link</button>
|
|
62
|
+
<span class="mailx-modal-spacer"></span>
|
|
63
|
+
<button type="button" class="mailx-modal-btn" data-action="cancel">Cancel</button>
|
|
64
|
+
<button type="button" class="mailx-modal-btn mailx-modal-btn-primary" data-action="ok">OK</button>
|
|
65
|
+
</div>`;
|
|
66
|
+
backdrop.appendChild(panel);
|
|
67
|
+
document.body.appendChild(backdrop);
|
|
68
|
+
|
|
69
|
+
const textInput = panel.querySelector<HTMLInputElement>("#mailx-link-text")!;
|
|
70
|
+
const urlInput = panel.querySelector<HTMLInputElement>("#mailx-link-url")!;
|
|
71
|
+
textInput.value = initialText;
|
|
72
|
+
urlInput.value = initialUrl;
|
|
73
|
+
|
|
74
|
+
const close = (result: { text: string; url: string; remove?: boolean } | null) => {
|
|
75
|
+
backdrop.remove();
|
|
76
|
+
document.removeEventListener("keydown", onKey, true);
|
|
77
|
+
resolve(result);
|
|
78
|
+
};
|
|
79
|
+
const commit = () => close({ text: textInput.value, url: normalizeUrl(urlInput.value) });
|
|
80
|
+
const onKey = (e: KeyboardEvent) => {
|
|
81
|
+
if (e.key === "Escape") { e.stopPropagation(); e.preventDefault(); close(null); }
|
|
82
|
+
else if (e.key === "Enter") { e.stopPropagation(); e.preventDefault(); commit(); }
|
|
83
|
+
};
|
|
84
|
+
document.addEventListener("keydown", onKey, true);
|
|
85
|
+
|
|
86
|
+
panel.querySelectorAll<HTMLButtonElement>(".mailx-modal-btn").forEach(btn => {
|
|
87
|
+
btn.addEventListener("click", () => {
|
|
88
|
+
const action = btn.dataset.action;
|
|
89
|
+
if (action === "cancel") close(null);
|
|
90
|
+
else if (action === "remove") close({ text: textInput.value, url: "", remove: true });
|
|
91
|
+
else commit();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
backdrop.addEventListener("mousedown", (e) => { if (e.target === backdrop) close(null); });
|
|
95
|
+
// Focus URL if we have text, else text first
|
|
96
|
+
(initialText ? urlInput : textInput).focus();
|
|
97
|
+
(initialText ? urlInput : textInput).select();
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function createQuillEditor(container: HTMLElement): MailxEditor {
|
|
102
|
+
/** Open the link dialog for the current selection or cursor. If range has
|
|
103
|
+
* a selection, prefill text from the selected text; if cursor is inside
|
|
104
|
+
* a link, prefill both from the link's range. */
|
|
105
|
+
const openLinkForRange = async (quill: any, range: any) => {
|
|
106
|
+
if (!range) return;
|
|
107
|
+
// Expand a bare cursor inside a link to the full link range
|
|
108
|
+
let linkRange = range;
|
|
109
|
+
const format = quill.getFormat(range);
|
|
110
|
+
if (range.length === 0 && format.link) {
|
|
111
|
+
// Walk left and right to find the link extent
|
|
112
|
+
const [leaf, offset] = quill.getLeaf(range.index);
|
|
113
|
+
if (leaf) {
|
|
114
|
+
// Build a range that spans the whole link
|
|
115
|
+
const text = quill.getText();
|
|
116
|
+
let start = range.index, end = range.index;
|
|
117
|
+
while (start > 0 && quill.getFormat(start - 1, 1).link === format.link) start--;
|
|
118
|
+
while (end < text.length - 1 && quill.getFormat(end, 1).link === format.link) end++;
|
|
119
|
+
linkRange = { index: start, length: end - start };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const currentText = linkRange.length ? quill.getText(linkRange.index, linkRange.length).replace(/\n$/, "") : "";
|
|
123
|
+
const currentUrl = format.link || "";
|
|
124
|
+
const result = await openLinkDialog(currentText, currentUrl);
|
|
125
|
+
if (!result) return;
|
|
126
|
+
if (result.remove) {
|
|
127
|
+
if (linkRange.length) quill.formatText(linkRange.index, linkRange.length, "link", false);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!result.url) return;
|
|
131
|
+
const newText = result.text || result.url;
|
|
132
|
+
if (linkRange.length) {
|
|
133
|
+
// Replace the existing text+link with new text+link
|
|
134
|
+
quill.deleteText(linkRange.index, linkRange.length);
|
|
135
|
+
quill.insertText(linkRange.index, newText, { link: result.url });
|
|
136
|
+
quill.setSelection(linkRange.index + newText.length, 0);
|
|
137
|
+
} else {
|
|
138
|
+
quill.insertText(linkRange.index, newText, { link: result.url });
|
|
139
|
+
quill.setSelection(linkRange.index + newText.length, 0);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Extra keybindings for formatting that Quill doesn't wire up by default.
|
|
144
|
+
// Ctrl+K (insert/edit link) is the one most users expect; we also add shortcuts
|
|
145
|
+
// for strikethrough, lists, indent, color, and clear-formatting.
|
|
146
|
+
const extraBindings: any = {
|
|
147
|
+
insertLink: {
|
|
148
|
+
key: "K", shortKey: true,
|
|
149
|
+
handler: function (this: any, range: any) {
|
|
150
|
+
openLinkForRange(this.quill, range);
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
removeLink: {
|
|
154
|
+
key: "K", shortKey: true, shiftKey: true,
|
|
155
|
+
handler: function (this: any) { this.quill.format("link", false); },
|
|
156
|
+
},
|
|
157
|
+
strike: {
|
|
158
|
+
key: "X", shortKey: true, shiftKey: true,
|
|
159
|
+
handler: function (this: any, range: any) {
|
|
160
|
+
if (!range) return true;
|
|
161
|
+
const cur = this.quill.getFormat(range).strike;
|
|
162
|
+
this.quill.format("strike", !cur);
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
orderedList: {
|
|
166
|
+
key: "7", shortKey: true, shiftKey: true,
|
|
167
|
+
handler: function (this: any) { this.quill.format("list", "ordered"); },
|
|
168
|
+
},
|
|
169
|
+
bulletList: {
|
|
170
|
+
key: "8", shortKey: true, shiftKey: true,
|
|
171
|
+
handler: function (this: any) { this.quill.format("list", "bullet"); },
|
|
172
|
+
},
|
|
173
|
+
indent: {
|
|
174
|
+
key: "]", shortKey: true,
|
|
175
|
+
handler: function (this: any, range: any, context: any) {
|
|
176
|
+
this.quill.format("indent", (context.format.indent || 0) + 1);
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
outdent: {
|
|
180
|
+
key: "[", shortKey: true,
|
|
181
|
+
handler: function (this: any, range: any, context: any) {
|
|
182
|
+
this.quill.format("indent", Math.max(0, (context.format.indent || 0) - 1));
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
color: {
|
|
186
|
+
key: "C", shortKey: true, shiftKey: true,
|
|
187
|
+
handler: function (this: any, range: any) {
|
|
188
|
+
if (!range) return true;
|
|
189
|
+
const current = this.quill.getFormat(range).color || "";
|
|
190
|
+
const color = prompt("Text color (name or #hex, blank to clear):", current);
|
|
191
|
+
if (color === null) return;
|
|
192
|
+
this.quill.format("color", color || false);
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
clearFormat: {
|
|
196
|
+
key: "\\", shortKey: true,
|
|
197
|
+
handler: function (this: any, range: any) {
|
|
198
|
+
if (!range) return true;
|
|
199
|
+
this.quill.removeFormat(range.index, range.length || 0);
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const q = new Quill(container, {
|
|
205
|
+
theme: "snow",
|
|
206
|
+
placeholder: "Write your message...",
|
|
207
|
+
modules: {
|
|
208
|
+
toolbar: [
|
|
209
|
+
[{ font: [] }, { size: ["small", false, "large", "huge"] }],
|
|
210
|
+
[{ header: [1, 2, 3, false] }],
|
|
211
|
+
["bold", "italic", "underline", "strike"],
|
|
212
|
+
[{ color: [] }, { background: [] }],
|
|
213
|
+
[{ list: "ordered" }, { list: "bullet" }],
|
|
214
|
+
[{ align: [] }],
|
|
215
|
+
["blockquote", "link", "image"],
|
|
216
|
+
["clean"]
|
|
217
|
+
],
|
|
218
|
+
keyboard: { bindings: extraBindings },
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Make toolbar buttons non-tabbable so Tab goes straight to editor body
|
|
223
|
+
document.querySelectorAll(".ql-toolbar button, .ql-toolbar select, .ql-toolbar .ql-picker-label").forEach(
|
|
224
|
+
el => (el as HTMLElement).setAttribute("tabindex", "-1")
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// Native spell-check: WebView2 / Chromium underlines misspellings and the
|
|
228
|
+
// right-click menu offers "Add to dictionary". Quill clears spellcheck on
|
|
229
|
+
// its root by default AND may re-clear it asynchronously as part of its
|
|
230
|
+
// setup (user-reported "spell-check broken again" with the one-shot set).
|
|
231
|
+
// Set once now, again after the frame flushes so post-init clears can't
|
|
232
|
+
// win, and install a MutationObserver to re-assert if anything later
|
|
233
|
+
// strips the attribute. Cheap — fires at most on each clear.
|
|
234
|
+
const applySpellcheck = () => {
|
|
235
|
+
if (q.root.getAttribute("spellcheck") !== "true") q.root.setAttribute("spellcheck", "true");
|
|
236
|
+
if (q.root.getAttribute("autocorrect") !== "on") q.root.setAttribute("autocorrect", "on");
|
|
237
|
+
if (q.root.getAttribute("autocapitalize") !== "on") q.root.setAttribute("autocapitalize", "on");
|
|
238
|
+
};
|
|
239
|
+
applySpellcheck();
|
|
240
|
+
requestAnimationFrame(applySpellcheck);
|
|
241
|
+
setTimeout(applySpellcheck, 100);
|
|
242
|
+
const spellObs = new MutationObserver(() => applySpellcheck());
|
|
243
|
+
spellObs.observe(q.root, { attributes: true, attributeFilter: ["spellcheck", "autocorrect", "autocapitalize"] });
|
|
244
|
+
|
|
245
|
+
// Toolbar link button: open our modal instead of Quill's built-in URL prompt.
|
|
246
|
+
const toolbar = q.getModule("toolbar");
|
|
247
|
+
toolbar?.addHandler("link", function (this: any) {
|
|
248
|
+
openLinkForRange(q, q.getSelection() || { index: q.getLength() - 1, length: 0 });
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Paste handling:
|
|
252
|
+
// - text/html clipboard with exactly one anchor (the common "copy a link
|
|
253
|
+
// with anchor text from a webpage" case): take it over from Quill —
|
|
254
|
+
// Quill's clipboard module was producing duplicates ("click here" as
|
|
255
|
+
// text PLUS a separate "https://example.com" as a link tail). Insert
|
|
256
|
+
// the anchor's text content as a single linked run.
|
|
257
|
+
// - text/html with richer content: defer to Quill (preserves formatting).
|
|
258
|
+
// - text/plain that's a URL: insert as a link, optionally wrapping any
|
|
259
|
+
// currently-selected text.
|
|
260
|
+
// - Anything else: default Quill behavior (verbatim plain or HTML).
|
|
261
|
+
// C36: AI proofread on right-click → "Proofread selection" item.
|
|
262
|
+
// Uses the existing aiTransform IPC. Gated by autocomplete.proofreadEnabled.
|
|
263
|
+
q.root.addEventListener("contextmenu", async (e: MouseEvent) => {
|
|
264
|
+
try {
|
|
265
|
+
const sel = q.getSelection();
|
|
266
|
+
if (!sel || sel.length === 0) return; // no selection — let native menu handle
|
|
267
|
+
const settingsRaw = localStorage.getItem("mailx-ai-proofread-enabled");
|
|
268
|
+
if (settingsRaw !== "true") return; // feature off
|
|
269
|
+
const text = q.getText(sel.index, sel.length);
|
|
270
|
+
if (!text.trim()) return;
|
|
271
|
+
e.preventDefault();
|
|
272
|
+
// Inline action menu next to the cursor
|
|
273
|
+
const menu = document.createElement("div");
|
|
274
|
+
menu.style.cssText = `position:fixed;z-index:2500;background:var(--color-bg);color:var(--color-text);border:1px solid var(--color-border);border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,0.2);padding:4px 0;font-size:13px;min-width:160px;`;
|
|
275
|
+
menu.style.left = `${e.clientX}px`;
|
|
276
|
+
menu.style.top = `${e.clientY}px`;
|
|
277
|
+
const item = document.createElement("div");
|
|
278
|
+
item.textContent = "Proofread selection";
|
|
279
|
+
item.style.cssText = `padding:6px 12px;cursor:pointer;`;
|
|
280
|
+
item.addEventListener("mouseenter", () => item.style.background = "var(--color-bg-hover)");
|
|
281
|
+
item.addEventListener("mouseleave", () => item.style.background = "");
|
|
282
|
+
item.addEventListener("click", async () => {
|
|
283
|
+
menu.remove();
|
|
284
|
+
try {
|
|
285
|
+
const { aiTransform } = await import("../lib/api-client.js");
|
|
286
|
+
const r = await aiTransform({ action: "proofread", text });
|
|
287
|
+
if (r?.text && r.text !== text) {
|
|
288
|
+
q.deleteText(sel.index, sel.length);
|
|
289
|
+
q.insertText(sel.index, r.text);
|
|
290
|
+
q.setSelection(sel.index, r.text.length);
|
|
291
|
+
} else if (r?.reason) {
|
|
292
|
+
alert(`Proofread: ${r.reason}`);
|
|
293
|
+
}
|
|
294
|
+
} catch (err: any) {
|
|
295
|
+
alert(`Proofread failed: ${err?.message || err}`);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
menu.appendChild(item);
|
|
299
|
+
document.body.appendChild(menu);
|
|
300
|
+
const dismiss = () => { menu.remove(); document.removeEventListener("mousedown", dismiss); };
|
|
301
|
+
setTimeout(() => document.addEventListener("mousedown", dismiss), 0);
|
|
302
|
+
} catch { /* fall through to native menu */ }
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// IMPORTANT: register on the capture phase AND use stopImmediatePropagation
|
|
306
|
+
// when we handle the paste ourselves. Quill 2.x's clipboard module attaches
|
|
307
|
+
// its own listener on the same root element; preventDefault only stops the
|
|
308
|
+
// browser's default contenteditable insertion, NOT Quill's parallel listener.
|
|
309
|
+
// Without stopImmediatePropagation the two handlers fire independently and
|
|
310
|
+
// both insert — user sees the URL twice. Capture phase guarantees we run
|
|
311
|
+
// before Quill so stopImmediatePropagation actually blocks it.
|
|
312
|
+
q.root.addEventListener("paste", (e: ClipboardEvent) => {
|
|
313
|
+
const cb = e.clipboardData;
|
|
314
|
+
if (!cb) return;
|
|
315
|
+
|
|
316
|
+
// Helper: call when we've handled the paste ourselves. Stops Quill's
|
|
317
|
+
// own listener from also processing the same event.
|
|
318
|
+
const consume = () => {
|
|
319
|
+
e.preventDefault();
|
|
320
|
+
e.stopImmediatePropagation();
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Q3: image-on-clipboard → inline as data: URL.
|
|
324
|
+
for (const item of Array.from(cb.items)) {
|
|
325
|
+
if (item.kind === "file" && item.type.startsWith("image/")) {
|
|
326
|
+
const file = item.getAsFile();
|
|
327
|
+
if (!file) continue;
|
|
328
|
+
consume();
|
|
329
|
+
const reader = new FileReader();
|
|
330
|
+
reader.onload = () => {
|
|
331
|
+
const dataUrl = String(reader.result || "");
|
|
332
|
+
const range = q.getSelection(true) || { index: q.getLength(), length: 0 };
|
|
333
|
+
q.insertEmbed(range.index, "image", dataUrl);
|
|
334
|
+
q.setSelection(range.index + 1, 0);
|
|
335
|
+
};
|
|
336
|
+
reader.readAsDataURL(file);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const html = cb.getData("text/html");
|
|
342
|
+
const plain = cb.getData("text/plain");
|
|
343
|
+
|
|
344
|
+
if (html) {
|
|
345
|
+
// Detect "single anchor" clipboard — copy from a browser usually
|
|
346
|
+
// produces something like:
|
|
347
|
+
// <meta charset='utf-8'><a href="https://example.com">click here</a>
|
|
348
|
+
// or wrapped in <html><body>. Parse and check.
|
|
349
|
+
try {
|
|
350
|
+
const tmp = document.createElement("div");
|
|
351
|
+
tmp.innerHTML = html;
|
|
352
|
+
// Strip script/style, then unwrap <html>/<body> noise.
|
|
353
|
+
const root = tmp.querySelector("body") || tmp;
|
|
354
|
+
// Walk for the only meaningful element
|
|
355
|
+
const meaningful = Array.from(root.childNodes).filter(n => {
|
|
356
|
+
if (n.nodeType === Node.TEXT_NODE) return (n.textContent || "").trim().length > 0;
|
|
357
|
+
if (n.nodeType === Node.ELEMENT_NODE) {
|
|
358
|
+
const tag = (n as Element).tagName.toLowerCase();
|
|
359
|
+
return tag !== "meta" && tag !== "style" && tag !== "script";
|
|
360
|
+
}
|
|
361
|
+
return false;
|
|
362
|
+
});
|
|
363
|
+
if (meaningful.length === 1 && (meaningful[0] as HTMLElement).tagName?.toLowerCase() === "a") {
|
|
364
|
+
const a = meaningful[0] as HTMLAnchorElement;
|
|
365
|
+
const href = a.getAttribute("href") || "";
|
|
366
|
+
const text = (a.textContent || "").trim();
|
|
367
|
+
if (href && text) {
|
|
368
|
+
consume();
|
|
369
|
+
const range = q.getSelection(true);
|
|
370
|
+
if (!range) return;
|
|
371
|
+
if (range.length > 0) {
|
|
372
|
+
// Selected text exists — replace with the linked anchor text
|
|
373
|
+
q.deleteText(range.index, range.length);
|
|
374
|
+
}
|
|
375
|
+
q.insertText(range.index, text, { link: href });
|
|
376
|
+
q.setSelection(range.index + text.length, 0);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// Single text-node wrapping the URL — common when copying from
|
|
381
|
+
// browser address bar (Chrome ships text/html as
|
|
382
|
+
// `<meta><span>URL</span>` alongside text/plain). Fall through
|
|
383
|
+
// to the plain-URL path below instead of letting Quill insert
|
|
384
|
+
// the bare URL text AND our handler insert it linked — which
|
|
385
|
+
// is exactly the double-paste the user reported.
|
|
386
|
+
const textOnly = (root.textContent || "").trim();
|
|
387
|
+
if (textOnly && looksLikeUrl(textOnly) && !root.querySelector("a")) {
|
|
388
|
+
consume();
|
|
389
|
+
const range = q.getSelection(true);
|
|
390
|
+
if (!range) return;
|
|
391
|
+
const url = normalizeUrl(textOnly);
|
|
392
|
+
if (range.length > 0) {
|
|
393
|
+
q.formatText(range.index, range.length, "link", url);
|
|
394
|
+
q.setSelection(range.index + range.length, 0);
|
|
395
|
+
} else {
|
|
396
|
+
q.insertText(range.index, textOnly, { link: url });
|
|
397
|
+
q.setSelection(range.index + textOnly.length, 0);
|
|
398
|
+
}
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
} catch { /* fall through to Quill default */ }
|
|
402
|
+
return; // Quill handles richer HTML clipboard (no consume → Quill runs)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (plain && looksLikeUrl(plain)) {
|
|
406
|
+
consume();
|
|
407
|
+
const range = q.getSelection(true);
|
|
408
|
+
if (!range) return;
|
|
409
|
+
const url = normalizeUrl(plain);
|
|
410
|
+
if (range.length > 0) {
|
|
411
|
+
// Preserve existing selection text, just format it as a link
|
|
412
|
+
q.formatText(range.index, range.length, "link", url);
|
|
413
|
+
q.setSelection(range.index + range.length, 0);
|
|
414
|
+
} else {
|
|
415
|
+
q.insertText(range.index, plain.trim(), { link: url });
|
|
416
|
+
q.setSelection(range.index + plain.trim().length, 0);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}, true); // capture=true — run before Quill's own paste listener
|
|
420
|
+
|
|
421
|
+
// Hover preview: show the target URL in a floating tooltip when the
|
|
422
|
+
// pointer is over a link. Built on top of native mouseover/mouseout
|
|
423
|
+
// rather than Quill's ql-tooltip (which is keyboard-triggered).
|
|
424
|
+
let hoverTip: HTMLElement | null = null;
|
|
425
|
+
q.root.addEventListener("mouseover", (e: MouseEvent) => {
|
|
426
|
+
const a = (e.target as HTMLElement).closest("a[href]") as HTMLAnchorElement | null;
|
|
427
|
+
if (!a) return;
|
|
428
|
+
if (hoverTip) hoverTip.remove();
|
|
429
|
+
hoverTip = document.createElement("div");
|
|
430
|
+
hoverTip.className = "mailx-link-hover";
|
|
431
|
+
hoverTip.textContent = a.getAttribute("href") || "";
|
|
432
|
+
document.body.appendChild(hoverTip);
|
|
433
|
+
const rect = a.getBoundingClientRect();
|
|
434
|
+
hoverTip.style.left = `${Math.max(8, rect.left)}px`;
|
|
435
|
+
hoverTip.style.top = `${rect.bottom + 4}px`;
|
|
436
|
+
});
|
|
437
|
+
q.root.addEventListener("mouseout", (e: MouseEvent) => {
|
|
438
|
+
const to = e.relatedTarget as HTMLElement | null;
|
|
439
|
+
if (to && to.closest("a[href]")) return;
|
|
440
|
+
if (hoverTip) { hoverTip.remove(); hoverTip = null; }
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
setHtml(html: string): void {
|
|
445
|
+
q.clipboard.dangerouslyPasteHTML(html);
|
|
446
|
+
},
|
|
447
|
+
getHtml(): string {
|
|
448
|
+
return q.root.innerHTML;
|
|
449
|
+
},
|
|
450
|
+
getText(): string {
|
|
451
|
+
return q.getText();
|
|
452
|
+
},
|
|
453
|
+
focus(): void {
|
|
454
|
+
q.focus();
|
|
455
|
+
},
|
|
456
|
+
setCursor(pos: number): void {
|
|
457
|
+
q.setSelection(pos, 0);
|
|
458
|
+
},
|
|
459
|
+
root: q.root,
|
|
460
|
+
getScrollContainer(): HTMLElement {
|
|
461
|
+
return q.root;
|
|
462
|
+
},
|
|
463
|
+
onContentChange(handler: () => void): void {
|
|
464
|
+
q.on("text-change", handler);
|
|
465
|
+
},
|
|
466
|
+
onKeyDown(handler: (e: KeyboardEvent) => void): void {
|
|
467
|
+
q.root.addEventListener("keydown", handler);
|
|
468
|
+
},
|
|
469
|
+
insertTextAtCursor(text: string): void {
|
|
470
|
+
const sel = q.getSelection();
|
|
471
|
+
if (sel) q.insertText(sel.index, text);
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ── tiptap ──
|
|
477
|
+
|
|
478
|
+
async function createTiptapEditor(container: HTMLElement): Promise<MailxEditor> {
|
|
479
|
+
// tiptap loaded via CDN — use global UMD bundles
|
|
480
|
+
const { Editor } = (window as any).tiptapCore;
|
|
481
|
+
const { StarterKit } = (window as any).tiptapStarterKit;
|
|
482
|
+
const { Link } = (window as any).tiptapExtensionLink;
|
|
483
|
+
const { Image } = (window as any).tiptapExtensionImage;
|
|
484
|
+
const { Underline } = (window as any).tiptapExtensionUnderline;
|
|
485
|
+
const { Placeholder } = (window as any).tiptapExtensionPlaceholder;
|
|
486
|
+
|
|
487
|
+
// Build toolbar
|
|
488
|
+
const toolbar = document.createElement("div");
|
|
489
|
+
toolbar.className = "tt-toolbar";
|
|
490
|
+
toolbar.innerHTML = `
|
|
491
|
+
<select class="tt-heading" tabindex="-1">
|
|
492
|
+
<option value="p">Normal</option>
|
|
493
|
+
<option value="1">Heading 1</option>
|
|
494
|
+
<option value="2">Heading 2</option>
|
|
495
|
+
<option value="3">Heading 3</option>
|
|
496
|
+
</select>
|
|
497
|
+
<button class="tt-btn" data-cmd="bold" title="Bold" tabindex="-1"><b>B</b></button>
|
|
498
|
+
<button class="tt-btn" data-cmd="italic" title="Italic" tabindex="-1"><i>I</i></button>
|
|
499
|
+
<button class="tt-btn" data-cmd="underline" title="Underline" tabindex="-1"><u>U</u></button>
|
|
500
|
+
<button class="tt-btn" data-cmd="strike" title="Strikethrough" tabindex="-1"><s>S</s></button>
|
|
501
|
+
<button class="tt-btn" data-cmd="bulletList" title="Bullet list" tabindex="-1">•</button>
|
|
502
|
+
<button class="tt-btn" data-cmd="orderedList" title="Ordered list" tabindex="-1">1.</button>
|
|
503
|
+
<button class="tt-btn" data-cmd="blockquote" title="Blockquote" tabindex="-1">“</button>
|
|
504
|
+
<button class="tt-btn" data-cmd="link" title="Link" tabindex="-1">🔗</button>
|
|
505
|
+
<button class="tt-btn" data-cmd="clearFormat" title="Clear formatting" tabindex="-1">⌧</button>
|
|
506
|
+
`;
|
|
507
|
+
|
|
508
|
+
// Content area
|
|
509
|
+
const content = document.createElement("div");
|
|
510
|
+
content.className = "tt-content";
|
|
511
|
+
|
|
512
|
+
container.appendChild(toolbar);
|
|
513
|
+
container.appendChild(content);
|
|
514
|
+
|
|
515
|
+
const ed = new Editor({
|
|
516
|
+
element: content,
|
|
517
|
+
extensions: [
|
|
518
|
+
StarterKit,
|
|
519
|
+
Link.configure({ openOnClick: false }),
|
|
520
|
+
Image,
|
|
521
|
+
Underline,
|
|
522
|
+
Placeholder.configure({ placeholder: "Write your message..." }),
|
|
523
|
+
],
|
|
524
|
+
content: "",
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// Wire toolbar buttons
|
|
528
|
+
toolbar.querySelectorAll(".tt-btn").forEach((btn: Element) => {
|
|
529
|
+
btn.addEventListener("mousedown", (e) => {
|
|
530
|
+
e.preventDefault();
|
|
531
|
+
const cmd = (btn as HTMLElement).dataset.cmd;
|
|
532
|
+
switch (cmd) {
|
|
533
|
+
case "bold": ed.chain().focus().toggleBold().run(); break;
|
|
534
|
+
case "italic": ed.chain().focus().toggleItalic().run(); break;
|
|
535
|
+
case "underline": ed.chain().focus().toggleUnderline().run(); break;
|
|
536
|
+
case "strike": ed.chain().focus().toggleStrike().run(); break;
|
|
537
|
+
case "bulletList": ed.chain().focus().toggleBulletList().run(); break;
|
|
538
|
+
case "orderedList": ed.chain().focus().toggleOrderedList().run(); break;
|
|
539
|
+
case "blockquote": ed.chain().focus().toggleBlockquote().run(); break;
|
|
540
|
+
case "link": {
|
|
541
|
+
const url = prompt("URL:");
|
|
542
|
+
if (url) ed.chain().focus().setLink({ href: url }).run();
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
case "clearFormat": ed.chain().focus().clearNodes().unsetAllMarks().run(); break;
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// Wire heading select
|
|
551
|
+
const headingSelect = toolbar.querySelector(".tt-heading") as HTMLSelectElement;
|
|
552
|
+
headingSelect?.addEventListener("change", () => {
|
|
553
|
+
const val = headingSelect.value;
|
|
554
|
+
if (val === "p") ed.chain().focus().setParagraph().run();
|
|
555
|
+
else ed.chain().focus().toggleHeading({ level: parseInt(val) as 1 | 2 | 3 }).run();
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
const editorEl = content.querySelector(".tiptap") as HTMLElement || content;
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
setHtml(html: string): void {
|
|
562
|
+
ed.commands.setContent(html);
|
|
563
|
+
},
|
|
564
|
+
getHtml(): string {
|
|
565
|
+
return ed.getHTML();
|
|
566
|
+
},
|
|
567
|
+
getText(): string {
|
|
568
|
+
return ed.getText();
|
|
569
|
+
},
|
|
570
|
+
focus(): void {
|
|
571
|
+
ed.commands.focus("start");
|
|
572
|
+
},
|
|
573
|
+
setCursor(pos: number): void {
|
|
574
|
+
ed.commands.focus("start");
|
|
575
|
+
},
|
|
576
|
+
root: editorEl,
|
|
577
|
+
getScrollContainer(): HTMLElement {
|
|
578
|
+
return editorEl;
|
|
579
|
+
},
|
|
580
|
+
onContentChange(handler: () => void): void {
|
|
581
|
+
ed.on("update", handler);
|
|
582
|
+
},
|
|
583
|
+
onKeyDown(handler: (e: KeyboardEvent) => void): void {
|
|
584
|
+
editorEl.addEventListener("keydown", handler);
|
|
585
|
+
},
|
|
586
|
+
insertTextAtCursor(text: string): void {
|
|
587
|
+
ed.commands.insertContent(text);
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// ── Factory ──
|
|
593
|
+
|
|
594
|
+
export async function createEditor(container: HTMLElement, type: "quill" | "tiptap"): Promise<MailxEditor> {
|
|
595
|
+
if (type === "tiptap") {
|
|
596
|
+
return createTiptapEditor(container);
|
|
597
|
+
}
|
|
598
|
+
return createQuillEditor(container);
|
|
599
|
+
}
|