@bobfrankston/rmfmail 1.2.254 → 1.2.256

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.
@@ -2699,7 +2699,11 @@ async function createTinyMceEditor(container2, opts = {}) {
2699
2699
  // left. Replaced the generic alignleft/center/right icons, which
2700
2700
  // don't communicate wrapping (Bob 2026-07-18: "the align is
2701
2701
  // confusing, better to use the Word icons for the three cases").
2702
- quickbars_image_toolbar: "rmfwrapinline rmfwrapleft rmfwrapright",
2702
+ // …plus a border control (Bob 2026-08-13: "image editing should have
2703
+ // an option to put a border around the image. It could be a check box
2704
+ // or a width size"). Split button: click = 1px border on/off, ▾ = pick
2705
+ // the width. Registered in setup below.
2706
+ quickbars_image_toolbar: "rmfwrapinline rmfwrapleft rmfwrapright | rmfborder",
2703
2707
  // Code-sample dropdown languages. TinyMCE's default list omits
2704
2708
  // "Text" / plain — every option triggers syntax highlighting
2705
2709
  // which mangles unrelated paste content (Bob 2026-05-24).
@@ -2718,6 +2722,11 @@ async function createTinyMceEditor(container2, opts = {}) {
2718
2722
  // OS file-picker the compose Attach button uses (<input type=file>,
2719
2723
  // which works in the msger WebView), then inlines the chosen image as
2720
2724
  // a base64 data URL — matching the drag-drop-an-image behavior.
2725
+ // Insert/Edit Image → Advanced tab: border width + style, and the
2726
+ // vertical/horizontal spacing that goes with a framed image. This is
2727
+ // the precise-control half of the border feature; the quickbar split
2728
+ // button below is the one-click half.
2729
+ image_advtab: true,
2721
2730
  file_picker_types: "image",
2722
2731
  file_picker_callback: (cb, _value, meta) => {
2723
2732
  if (meta && meta.filetype && meta.filetype !== "image")
@@ -2874,6 +2883,56 @@ async function createTinyMceEditor(container2, opts = {}) {
2874
2883
  wrapButton("rmfwrapinline", "rmf-wrap-inline", "In line with text", "inline");
2875
2884
  wrapButton("rmfwrapleft", "rmf-wrap-left", "Image left \u2014 text wraps around the right", "left");
2876
2885
  wrapButton("rmfwrapright", "rmf-wrap-right", "Image right \u2014 text wraps around the left", "right");
2886
+ const BORDER_COLOR = "#888888";
2887
+ const BORDER_WIDTHS = [1, 2, 3, 5];
2888
+ ed.ui.registry.addIcon("rmf-border", `<svg width="24" height="24" viewBox="0 0 24 24"><rect x="3.5" y="4.5" width="17" height="15" rx="1" fill="none" stroke="currentColor" stroke-width="2"/>` + arch(12, 16.5) + `</svg>`);
2889
+ const borderWidthOf = (node) => {
2890
+ if (!node || node.nodeName !== "IMG")
2891
+ return 0;
2892
+ const style = node.style?.borderStyle || "";
2893
+ if (!style || style === "none")
2894
+ return 0;
2895
+ const w = parseFloat(node.style?.borderWidth || "");
2896
+ return Number.isFinite(w) ? w : 0;
2897
+ };
2898
+ const applyBorder = (px) => {
2899
+ const node = ed.selection.getNode();
2900
+ if (!node || node.nodeName !== "IMG")
2901
+ return;
2902
+ ed.undoManager.transact(() => {
2903
+ ed.dom.setStyles(node, px > 0 ? { border: `${px}px solid ${BORDER_COLOR}` } : { border: "" });
2904
+ });
2905
+ ed.nodeChanged();
2906
+ };
2907
+ ed.ui.registry.addSplitButton("rmfborder", {
2908
+ icon: "rmf-border",
2909
+ tooltip: "Border around the image \u2014 click to toggle, \u25BE to pick the width",
2910
+ // Click toggles: bordered → none, unbordered → 1px hairline.
2911
+ onAction: () => applyBorder(borderWidthOf(ed.selection.getNode()) > 0 ? 0 : 1),
2912
+ onItemAction: (_api, value) => applyBorder(Number(value)),
2913
+ select: (value) => Number(value) === borderWidthOf(ed.selection.getNode()),
2914
+ fetch: (cb) => cb([
2915
+ { type: "choiceitem", text: "No border", value: "0" },
2916
+ ...BORDER_WIDTHS.map((w) => ({ type: "choiceitem", text: `${w} px`, value: String(w) }))
2917
+ ]),
2918
+ onSetup: (api) => {
2919
+ const update = () => api.setActive(borderWidthOf(ed.selection.getNode()) > 0);
2920
+ ed.on("NodeChange", update);
2921
+ return () => ed.off("NodeChange", update);
2922
+ }
2923
+ });
2924
+ ed.on("NodeChange", () => {
2925
+ const n = ed.selection.getNode();
2926
+ if (!n || n.nodeName !== "IMG")
2927
+ return;
2928
+ const w = parseFloat(n.style?.borderWidth || "");
2929
+ const s = n.style?.borderStyle || "";
2930
+ if (Number.isFinite(w) && w > 0 && (!s || s === "none")) {
2931
+ ed.dom.setStyle(n, "border-style", "solid");
2932
+ if (!n.style.borderColor)
2933
+ ed.dom.setStyle(n, "border-color", BORDER_COLOR);
2934
+ }
2935
+ });
2877
2936
  ed.on("paste", (e) => {
2878
2937
  const cb = e.clipboardData;
2879
2938
  if (!cb)
@@ -3268,7 +3327,18 @@ async function createTinyMceEditor(container2, opts = {}) {
3268
3327
  function openSourceDialog(editor2) {
3269
3328
  const SENTINEL = "\uE000";
3270
3329
  let offset = -1;
3330
+ let savedRng = null;
3271
3331
  try {
3332
+ const sel = editor2.selection;
3333
+ const rng = sel.getRng();
3334
+ if (rng) {
3335
+ savedRng = rng.cloneRange();
3336
+ if (!rng.collapsed) {
3337
+ const collapsed = rng.cloneRange();
3338
+ collapsed.collapse(true);
3339
+ sel.setRng(collapsed);
3340
+ }
3341
+ }
3272
3342
  const insert = () => editor2.selection.setContent(SENTINEL);
3273
3343
  if (typeof editor2.undoManager?.ignore === "function")
3274
3344
  editor2.undoManager.ignore(insert);
@@ -3289,6 +3359,12 @@ function openSourceDialog(editor2) {
3289
3359
  }
3290
3360
  } catch {
3291
3361
  }
3362
+ if (savedRng) {
3363
+ try {
3364
+ editor2.selection.setRng(savedRng);
3365
+ } catch {
3366
+ }
3367
+ }
3292
3368
  } catch {
3293
3369
  }
3294
3370
  const source = editor2.getContent();