smeditor 0.2.3 → 0.2.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e09edfe28f68de93439fdd0552894387afc89e6eadf4875fb164d1a45aaa7658
4
- data.tar.gz: a76689f1341ffd28d5b9e2fa24bf3c6543c40a0b9f8cc83ccc3348c871c41de5
3
+ metadata.gz: 3edf61aad6834f2726ef15563ef798a7d71ae7758868f016fcd82af1b3521e39
4
+ data.tar.gz: 4ef021f5c72395a43f3f6dac47e5694c425bb4051691830f12ff83425f7e40e6
5
5
  SHA512:
6
- metadata.gz: 06f5e3c97c24a257a36d115e3ceebc42f0ab70060c9e5013d81807ac0d77af20a218ea6597972724e1df5492e26733eb382cedf0fbb638ecab90839f86d3d1ab
7
- data.tar.gz: 36f25a97de694d48cff314de6bffef35bd6016d2960489601881ad35ef893ce580a3b4a3a4ba86c395e13bca62140b236170d985a6f3bc0f0ebcd19043aca43a
6
+ metadata.gz: dbf99306fc6c0e519bc46f3de5910d7508f24d4016afde2aea1c713380db598e7145338b778d44c284f94902a1a5f31ca35e95e099355c8563e29a6f842930b3
7
+ data.tar.gz: fda2e73e1c3df37b94c77911b0cf63c27d1d2d9bfa3488553a58b45f356601e3d7f015e524a847acc5878535ff5a5af33518310a4e52b3f583cc14fdb378c475
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog — smeditor
2
2
 
3
+ ## 0.2.4 — 2026-09-09
4
+
5
+ - Added the selected-text bubble formatting toolbar to the self-contained Rails adapter, matching the playground behavior.
6
+ - Bubble toolbar positioning and selection tracking now work inside the Rails Shadow DOM and stay visible across scroll/resize.
7
+ - Core selection syncing now also listens on the editor ShadowRoot when available.
8
+
3
9
  ## 0.2.3
4
10
 
5
11
  - Added configurable responsive minimum editor heights for desktop, tablet, and mobile.
@@ -446,6 +446,120 @@ function buildToolbar(editor, kit, uploadUrl) {
446
446
  };
447
447
  return toolbar;
448
448
  }
449
+ function selectionForSurface(surface) {
450
+ var _a, _b, _c, _d;
451
+ const root = (_a = surface.getRootNode) === null || _a === void 0 ? void 0 : _a.call(surface);
452
+ const getSelection = root === null || root === void 0 ? void 0 : root.getSelection;
453
+ if (root && typeof getSelection === "function") {
454
+ const selection = getSelection.call(root);
455
+ if (selection)
456
+ return selection;
457
+ }
458
+ return (_d = (_c = (_b = surface.ownerDocument) === null || _b === void 0 ? void 0 : _b.getSelection) === null || _c === void 0 ? void 0 : _c.call(_b)) !== null && _d !== void 0 ? _d : null;
459
+ }
460
+ /**
461
+ * Rails uses a dependency-free browser adapter rather than the React package,
462
+ * so contextual UI that exists in the playground must be mounted explicitly.
463
+ * This mirrors React's <BubbleMenu />: selecting non-empty text opens a small
464
+ * formatting toolbar above the live DOM range.
465
+ */
466
+ function buildBubbleMenu(editor, surface) {
467
+ var _a;
468
+ const menu = document.createElement("div");
469
+ menu.className = "smeditor-floating smeditor-bubble-menu";
470
+ menu.setAttribute("role", "toolbar");
471
+ menu.setAttribute("aria-label", "Selected text formatting");
472
+ menu.style.position = "fixed";
473
+ menu.style.display = "none";
474
+ menu.style.zIndex = "2147483000";
475
+ const buttons = [
476
+ commandButton(editor, "bold", "Bold", () => run(editor, "toggleBold"), () => editor.isActive("bold")),
477
+ commandButton(editor, "italic", "Italic", () => run(editor, "toggleItalic"), () => editor.isActive("italic")),
478
+ commandButton(editor, "underline", "Underline", () => run(editor, "toggleUnderline"), () => editor.isActive("underline")),
479
+ commandButton(editor, "strike", "Strike-through", () => run(editor, "toggleStrike"), () => editor.isActive("strike")),
480
+ commandButton(editor, "link", "Link", () => {
481
+ if (editor.isActive("link"))
482
+ return run(editor, "unsetLink");
483
+ const href = window.prompt("Link URL", "https://");
484
+ return href ? run(editor, "setLink", { href }) : false;
485
+ }, () => editor.isActive("link")),
486
+ ];
487
+ menu.append(...buttons);
488
+ // Keeping pointer-down inside the menu from focusing a button preserves the
489
+ // editor's text selection, which the formatting command needs.
490
+ menu.addEventListener("mousedown", (event) => event.preventDefault());
491
+ let frame = 0;
492
+ const hide = () => {
493
+ menu.style.display = "none";
494
+ menu.style.visibility = "hidden";
495
+ };
496
+ const updateNow = () => {
497
+ var _a, _b;
498
+ frame = 0;
499
+ if (surface.getAttribute("contenteditable") === "false")
500
+ return hide();
501
+ const selection = selectionForSurface(surface);
502
+ if (!selection || selection.rangeCount === 0 || selection.isCollapsed)
503
+ return hide();
504
+ const range = selection.getRangeAt(0);
505
+ if (!surface.contains(range.commonAncestorContainer))
506
+ return hide();
507
+ const anchor = range.getBoundingClientRect();
508
+ if (anchor.width === 0 && anchor.height === 0)
509
+ return hide();
510
+ for (const button of buttons)
511
+ (_b = (_a = button).__smeditorRefresh) === null || _b === void 0 ? void 0 : _b.call(_a);
512
+ // Make it measurable off-screen, then place it before the next paint.
513
+ menu.style.display = "inline-flex";
514
+ menu.style.visibility = "hidden";
515
+ menu.style.left = "-9999px";
516
+ menu.style.top = "-9999px";
517
+ const rect = menu.getBoundingClientRect();
518
+ const margin = 8;
519
+ const offset = 8;
520
+ const viewportWidth = window.innerWidth;
521
+ const viewportHeight = window.innerHeight;
522
+ let left = anchor.left + anchor.width / 2 - rect.width / 2;
523
+ left = Math.max(margin, Math.min(left, viewportWidth - rect.width - margin));
524
+ let top = anchor.top - rect.height - offset;
525
+ if (top < margin)
526
+ top = anchor.bottom + offset;
527
+ if (top + rect.height > viewportHeight - margin) {
528
+ top = Math.max(margin, anchor.top - rect.height - offset);
529
+ }
530
+ menu.style.left = `${Math.round(left)}px`;
531
+ menu.style.top = `${Math.round(top)}px`;
532
+ menu.style.visibility = "visible";
533
+ };
534
+ const scheduleUpdate = () => {
535
+ if (frame)
536
+ cancelAnimationFrame(frame);
537
+ frame = requestAnimationFrame(updateNow);
538
+ };
539
+ const selectionRoot = (_a = surface.getRootNode) === null || _a === void 0 ? void 0 : _a.call(surface);
540
+ const selectionTarget = selectionRoot && selectionRoot !== document
541
+ ? selectionRoot
542
+ : null;
543
+ const disposeSelection = editor.on("selectionUpdate", scheduleUpdate);
544
+ document.addEventListener("selectionchange", scheduleUpdate);
545
+ selectionTarget === null || selectionTarget === void 0 ? void 0 : selectionTarget.addEventListener("selectionchange", scheduleUpdate);
546
+ surface.addEventListener("pointerup", scheduleUpdate);
547
+ surface.addEventListener("keyup", scheduleUpdate);
548
+ window.addEventListener("resize", scheduleUpdate);
549
+ window.addEventListener("scroll", scheduleUpdate, true);
550
+ menu.__smeditorDispose = () => {
551
+ if (frame)
552
+ cancelAnimationFrame(frame);
553
+ disposeSelection();
554
+ document.removeEventListener("selectionchange", scheduleUpdate);
555
+ selectionTarget === null || selectionTarget === void 0 ? void 0 : selectionTarget.removeEventListener("selectionchange", scheduleUpdate);
556
+ surface.removeEventListener("pointerup", scheduleUpdate);
557
+ surface.removeEventListener("keyup", scheduleUpdate);
558
+ window.removeEventListener("resize", scheduleUpdate);
559
+ window.removeEventListener("scroll", scheduleUpdate, true);
560
+ };
561
+ return menu;
562
+ }
449
563
  function shadowRootFor(mount) {
450
564
  const root = mount.shadowRoot || mount.attachShadow({ mode: "open" });
451
565
  root.innerHTML = "";
@@ -504,11 +618,16 @@ function bootMount(mount) {
504
618
  });
505
619
  const toolbar = buildToolbar(editor, kit, uploadUrl);
506
620
  shell.insertBefore(toolbar, surface);
621
+ const bubbleMenu = buildBubbleMenu(editor, surface);
622
+ root.appendChild(bubbleMenu);
507
623
  input.smeditorInstance = editor;
508
624
  mount.smeditorInstance = editor;
509
625
  mount.dataset.smeditorBooted = "1";
510
626
  instances.set(mount, editor);
511
- disposers.set(mount, [() => { var _a, _b; return (_b = (_a = toolbar).__smeditorDispose) === null || _b === void 0 ? void 0 : _b.call(_a); }]);
627
+ disposers.set(mount, [
628
+ () => { var _a, _b; return (_b = (_a = toolbar).__smeditorDispose) === null || _b === void 0 ? void 0 : _b.call(_a); },
629
+ () => { var _a, _b; return (_b = (_a = bubbleMenu).__smeditorDispose) === null || _b === void 0 ? void 0 : _b.call(_a); },
630
+ ]);
512
631
  }
513
632
  function boot(root = document) {
514
633
  root.querySelectorAll("[data-smeditor]").forEach(bootMount);
@@ -690,6 +809,7 @@ class Editor {
690
809
  this.onDomFocus = () => this.emit("focus", { editor: this });
691
810
  this.onDomBlur = () => this.emit("blur", { editor: this });
692
811
  this.onSelectionChange = () => this.syncSelectionFromDOM();
812
+ this.selectionChangeTargets = [];
693
813
  this.onCompositionStart = () => {
694
814
  this.composing = true;
695
815
  };
@@ -997,6 +1117,7 @@ class Editor {
997
1117
  this.attachToElement();
998
1118
  }
999
1119
  attachToElement() {
1120
+ var _a;
1000
1121
  const el = this.element;
1001
1122
  el.setAttribute("contenteditable", String(this.options.editable !== false));
1002
1123
  el.classList.add("smeditor-editor");
@@ -1009,7 +1130,17 @@ class Editor {
1009
1130
  el.addEventListener("compositionstart", this.onCompositionStart);
1010
1131
  el.addEventListener("compositionend", this.onCompositionEnd);
1011
1132
  if (typeof document !== "undefined") {
1012
- document.addEventListener("selectionchange", this.onSelectionChange);
1133
+ // Keep the model selection in sync in both regular DOM and Shadow DOM.
1134
+ // Chromium exposes the live shadow selection on ShadowRoot; listening to
1135
+ // that root as well as Document avoids missing drag-selection events.
1136
+ const root = (_a = el.getRootNode) === null || _a === void 0 ? void 0 : _a.call(el);
1137
+ const targets = [document];
1138
+ if (root && root !== document && "addEventListener" in root)
1139
+ targets.push(root);
1140
+ for (const target of targets) {
1141
+ target.addEventListener("selectionchange", this.onSelectionChange);
1142
+ this.selectionChangeTargets.push(target);
1143
+ }
1013
1144
  }
1014
1145
  this.renderToDOM();
1015
1146
  }
@@ -1346,9 +1477,10 @@ class Editor {
1346
1477
  this.element.removeEventListener("compositionstart", this.onCompositionStart);
1347
1478
  this.element.removeEventListener("compositionend", this.onCompositionEnd);
1348
1479
  }
1349
- if (typeof document !== "undefined") {
1350
- document.removeEventListener("selectionchange", this.onSelectionChange);
1480
+ for (const target of this.selectionChangeTargets) {
1481
+ target.removeEventListener("selectionchange", this.onSelectionChange);
1351
1482
  }
1483
+ this.selectionChangeTargets.length = 0;
1352
1484
  for (const ext of this.extensions) {
1353
1485
  (_a = ext.onDestroy) === null || _a === void 0 ? void 0 : _a.call(ext, this);
1354
1486
  }
@@ -2,6 +2,6 @@
2
2
 
3
3
  module SMEditor
4
4
  module Rails
5
- VERSION = "0.2.3"
5
+ VERSION = "0.2.4"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smeditor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - SMEditor contributors
@@ -96,7 +96,7 @@ metadata:
96
96
  source_code_uri: https://github.com/sCruze/smeditor/tree/main/gems/smeditor
97
97
  changelog_uri: https://github.com/sCruze/smeditor/blob/main/gems/smeditor/CHANGELOG.md
98
98
  bug_tracker_uri: https://github.com/sCruze/smeditor/issues
99
- documentation_uri: https://rubydoc.info/gems/smeditor/0.2.3
99
+ documentation_uri: https://rubydoc.info/gems/smeditor/0.2.4
100
100
  rubygems_mfa_required: 'true'
101
101
  rdoc_options: []
102
102
  require_paths: