smeditor 0.2.3 → 0.2.5
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 +4 -4
- data/CHANGELOG.md +12 -0
- data/app/assets/javascripts/smeditor.js +148 -4
- data/app/assets/stylesheets/smeditor.css +1 -0
- data/lib/smeditor/rails/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b78483ba31701d6ff1da3c082c82396b6612812174f756d83312b13dc9f2a16d
|
|
4
|
+
data.tar.gz: d14fac8b9b6a172bf23fddf19eb0fa6b34b3eae978884ed7cc6b70b29a8c7e2e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f05b0e4d311cc29ab173c12b84d745fb8ef50c3a956d1983bf619a901fc738157cabcfe9784a197c28ff8991f88ccedc5b74af38a161aab1b63474b19c6c0841
|
|
7
|
+
data.tar.gz: c7ed0efe7cddecbde87182b620027ccfed65002948e14f4f68226e7ba3dc92bd1df095cd0445d3943e37193e2afd5a32159a34ff7ed19936aac5fad4104113a0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog — smeditor
|
|
2
2
|
|
|
3
|
+
## 0.2.5 — 2026-09-09
|
|
4
|
+
|
|
5
|
+
- Fixed the Rails selected-text bubble toolbar being displaced to the right or vertically aligned with the text when the host application creates a CSS containing block (for example with `transform`, `filter`, `perspective`, or modal/animated wrappers).
|
|
6
|
+
- Bubble positioning now matches the playground: it is centered above the live selection, flips below near the top viewport edge, and clamps horizontally to the viewport.
|
|
7
|
+
- The Rails bubble is positioned in editor-local coordinates, so host layout transforms no longer corrupt its placement.
|
|
8
|
+
|
|
9
|
+
## 0.2.4 — 2026-09-09
|
|
10
|
+
|
|
11
|
+
- Added the selected-text bubble formatting toolbar to the self-contained Rails adapter, matching the playground behavior.
|
|
12
|
+
- Bubble toolbar positioning and selection tracking now work inside the Rails Shadow DOM and stay visible across scroll/resize.
|
|
13
|
+
- Core selection syncing now also listens on the editor ShadowRoot when available.
|
|
14
|
+
|
|
3
15
|
## 0.2.3
|
|
4
16
|
|
|
5
17
|
- Added configurable responsive minimum editor heights for desktop, tablet, and mobile.
|
|
@@ -446,6 +446,132 @@ 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, container) {
|
|
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
|
+
// Keep the popover in the editor's own coordinate space. `position: fixed`
|
|
473
|
+
// becomes relative to transformed/filtering ancestors in browsers, which can
|
|
474
|
+
// shove the menu to the right or down in host applications. Absolute local
|
|
475
|
+
// positioning is stable because both the selection and container are measured
|
|
476
|
+
// in viewport coordinates and then converted to editor-local coordinates.
|
|
477
|
+
menu.style.position = "absolute";
|
|
478
|
+
menu.style.display = "none";
|
|
479
|
+
menu.style.zIndex = "2147483000";
|
|
480
|
+
const buttons = [
|
|
481
|
+
commandButton(editor, "bold", "Bold", () => run(editor, "toggleBold"), () => editor.isActive("bold")),
|
|
482
|
+
commandButton(editor, "italic", "Italic", () => run(editor, "toggleItalic"), () => editor.isActive("italic")),
|
|
483
|
+
commandButton(editor, "underline", "Underline", () => run(editor, "toggleUnderline"), () => editor.isActive("underline")),
|
|
484
|
+
commandButton(editor, "strike", "Strike-through", () => run(editor, "toggleStrike"), () => editor.isActive("strike")),
|
|
485
|
+
commandButton(editor, "link", "Link", () => {
|
|
486
|
+
if (editor.isActive("link"))
|
|
487
|
+
return run(editor, "unsetLink");
|
|
488
|
+
const href = window.prompt("Link URL", "https://");
|
|
489
|
+
return href ? run(editor, "setLink", { href }) : false;
|
|
490
|
+
}, () => editor.isActive("link")),
|
|
491
|
+
];
|
|
492
|
+
menu.append(...buttons);
|
|
493
|
+
// Keeping pointer-down inside the menu from focusing a button preserves the
|
|
494
|
+
// editor's text selection, which the formatting command needs.
|
|
495
|
+
menu.addEventListener("mousedown", (event) => event.preventDefault());
|
|
496
|
+
let frame = 0;
|
|
497
|
+
const hide = () => {
|
|
498
|
+
menu.style.display = "none";
|
|
499
|
+
menu.style.visibility = "hidden";
|
|
500
|
+
};
|
|
501
|
+
const updateNow = () => {
|
|
502
|
+
var _a, _b;
|
|
503
|
+
frame = 0;
|
|
504
|
+
if (surface.getAttribute("contenteditable") === "false")
|
|
505
|
+
return hide();
|
|
506
|
+
const selection = selectionForSurface(surface);
|
|
507
|
+
if (!selection || selection.rangeCount === 0 || selection.isCollapsed)
|
|
508
|
+
return hide();
|
|
509
|
+
const range = selection.getRangeAt(0);
|
|
510
|
+
if (!surface.contains(range.commonAncestorContainer))
|
|
511
|
+
return hide();
|
|
512
|
+
const anchor = range.getBoundingClientRect();
|
|
513
|
+
if (anchor.width === 0 && anchor.height === 0)
|
|
514
|
+
return hide();
|
|
515
|
+
for (const button of buttons)
|
|
516
|
+
(_b = (_a = button).__smeditorRefresh) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
517
|
+
// Make it measurable off-screen, then place it before the next paint.
|
|
518
|
+
menu.style.display = "inline-flex";
|
|
519
|
+
menu.style.visibility = "hidden";
|
|
520
|
+
menu.style.left = "-9999px";
|
|
521
|
+
menu.style.top = "-9999px";
|
|
522
|
+
const rect = menu.getBoundingClientRect();
|
|
523
|
+
const containerRect = container.getBoundingClientRect();
|
|
524
|
+
const margin = 8;
|
|
525
|
+
const offset = 8;
|
|
526
|
+
const viewportWidth = window.innerWidth;
|
|
527
|
+
const viewportHeight = window.innerHeight;
|
|
528
|
+
// Calculate the desired position in viewport coordinates first, exactly like
|
|
529
|
+
// the React playground's Floating primitive, including viewport clamping and
|
|
530
|
+
// top/bottom collision handling. Then translate that point into the local
|
|
531
|
+
// coordinate system of the editor shell. This survives transformed parents,
|
|
532
|
+
// zoomed panels, modals, sidebars and other containing-block creators.
|
|
533
|
+
let viewportLeft = anchor.left + anchor.width / 2 - rect.width / 2;
|
|
534
|
+
const maxViewportLeft = Math.max(margin, viewportWidth - rect.width - margin);
|
|
535
|
+
viewportLeft = Math.max(margin, Math.min(viewportLeft, maxViewportLeft));
|
|
536
|
+
let viewportTop = anchor.top - rect.height - offset;
|
|
537
|
+
if (viewportTop < margin)
|
|
538
|
+
viewportTop = anchor.bottom + offset;
|
|
539
|
+
if (viewportTop + rect.height > viewportHeight - margin) {
|
|
540
|
+
viewportTop = Math.max(margin, anchor.top - rect.height - offset);
|
|
541
|
+
}
|
|
542
|
+
menu.style.left = `${Math.round(viewportLeft - containerRect.left)}px`;
|
|
543
|
+
menu.style.top = `${Math.round(viewportTop - containerRect.top)}px`;
|
|
544
|
+
menu.style.visibility = "visible";
|
|
545
|
+
};
|
|
546
|
+
const scheduleUpdate = () => {
|
|
547
|
+
if (frame)
|
|
548
|
+
cancelAnimationFrame(frame);
|
|
549
|
+
frame = requestAnimationFrame(updateNow);
|
|
550
|
+
};
|
|
551
|
+
const selectionRoot = (_a = surface.getRootNode) === null || _a === void 0 ? void 0 : _a.call(surface);
|
|
552
|
+
const selectionTarget = selectionRoot && selectionRoot !== document
|
|
553
|
+
? selectionRoot
|
|
554
|
+
: null;
|
|
555
|
+
const disposeSelection = editor.on("selectionUpdate", scheduleUpdate);
|
|
556
|
+
document.addEventListener("selectionchange", scheduleUpdate);
|
|
557
|
+
selectionTarget === null || selectionTarget === void 0 ? void 0 : selectionTarget.addEventListener("selectionchange", scheduleUpdate);
|
|
558
|
+
surface.addEventListener("pointerup", scheduleUpdate);
|
|
559
|
+
surface.addEventListener("keyup", scheduleUpdate);
|
|
560
|
+
window.addEventListener("resize", scheduleUpdate);
|
|
561
|
+
window.addEventListener("scroll", scheduleUpdate, true);
|
|
562
|
+
menu.__smeditorDispose = () => {
|
|
563
|
+
if (frame)
|
|
564
|
+
cancelAnimationFrame(frame);
|
|
565
|
+
disposeSelection();
|
|
566
|
+
document.removeEventListener("selectionchange", scheduleUpdate);
|
|
567
|
+
selectionTarget === null || selectionTarget === void 0 ? void 0 : selectionTarget.removeEventListener("selectionchange", scheduleUpdate);
|
|
568
|
+
surface.removeEventListener("pointerup", scheduleUpdate);
|
|
569
|
+
surface.removeEventListener("keyup", scheduleUpdate);
|
|
570
|
+
window.removeEventListener("resize", scheduleUpdate);
|
|
571
|
+
window.removeEventListener("scroll", scheduleUpdate, true);
|
|
572
|
+
};
|
|
573
|
+
return menu;
|
|
574
|
+
}
|
|
449
575
|
function shadowRootFor(mount) {
|
|
450
576
|
const root = mount.shadowRoot || mount.attachShadow({ mode: "open" });
|
|
451
577
|
root.innerHTML = "";
|
|
@@ -504,11 +630,16 @@ function bootMount(mount) {
|
|
|
504
630
|
});
|
|
505
631
|
const toolbar = buildToolbar(editor, kit, uploadUrl);
|
|
506
632
|
shell.insertBefore(toolbar, surface);
|
|
633
|
+
const bubbleMenu = buildBubbleMenu(editor, surface, shell);
|
|
634
|
+
shell.appendChild(bubbleMenu);
|
|
507
635
|
input.smeditorInstance = editor;
|
|
508
636
|
mount.smeditorInstance = editor;
|
|
509
637
|
mount.dataset.smeditorBooted = "1";
|
|
510
638
|
instances.set(mount, editor);
|
|
511
|
-
disposers.set(mount, [
|
|
639
|
+
disposers.set(mount, [
|
|
640
|
+
() => { var _a, _b; return (_b = (_a = toolbar).__smeditorDispose) === null || _b === void 0 ? void 0 : _b.call(_a); },
|
|
641
|
+
() => { var _a, _b; return (_b = (_a = bubbleMenu).__smeditorDispose) === null || _b === void 0 ? void 0 : _b.call(_a); },
|
|
642
|
+
]);
|
|
512
643
|
}
|
|
513
644
|
function boot(root = document) {
|
|
514
645
|
root.querySelectorAll("[data-smeditor]").forEach(bootMount);
|
|
@@ -690,6 +821,7 @@ class Editor {
|
|
|
690
821
|
this.onDomFocus = () => this.emit("focus", { editor: this });
|
|
691
822
|
this.onDomBlur = () => this.emit("blur", { editor: this });
|
|
692
823
|
this.onSelectionChange = () => this.syncSelectionFromDOM();
|
|
824
|
+
this.selectionChangeTargets = [];
|
|
693
825
|
this.onCompositionStart = () => {
|
|
694
826
|
this.composing = true;
|
|
695
827
|
};
|
|
@@ -997,6 +1129,7 @@ class Editor {
|
|
|
997
1129
|
this.attachToElement();
|
|
998
1130
|
}
|
|
999
1131
|
attachToElement() {
|
|
1132
|
+
var _a;
|
|
1000
1133
|
const el = this.element;
|
|
1001
1134
|
el.setAttribute("contenteditable", String(this.options.editable !== false));
|
|
1002
1135
|
el.classList.add("smeditor-editor");
|
|
@@ -1009,7 +1142,17 @@ class Editor {
|
|
|
1009
1142
|
el.addEventListener("compositionstart", this.onCompositionStart);
|
|
1010
1143
|
el.addEventListener("compositionend", this.onCompositionEnd);
|
|
1011
1144
|
if (typeof document !== "undefined") {
|
|
1012
|
-
|
|
1145
|
+
// Keep the model selection in sync in both regular DOM and Shadow DOM.
|
|
1146
|
+
// Chromium exposes the live shadow selection on ShadowRoot; listening to
|
|
1147
|
+
// that root as well as Document avoids missing drag-selection events.
|
|
1148
|
+
const root = (_a = el.getRootNode) === null || _a === void 0 ? void 0 : _a.call(el);
|
|
1149
|
+
const targets = [document];
|
|
1150
|
+
if (root && root !== document && "addEventListener" in root)
|
|
1151
|
+
targets.push(root);
|
|
1152
|
+
for (const target of targets) {
|
|
1153
|
+
target.addEventListener("selectionchange", this.onSelectionChange);
|
|
1154
|
+
this.selectionChangeTargets.push(target);
|
|
1155
|
+
}
|
|
1013
1156
|
}
|
|
1014
1157
|
this.renderToDOM();
|
|
1015
1158
|
}
|
|
@@ -1346,9 +1489,10 @@ class Editor {
|
|
|
1346
1489
|
this.element.removeEventListener("compositionstart", this.onCompositionStart);
|
|
1347
1490
|
this.element.removeEventListener("compositionend", this.onCompositionEnd);
|
|
1348
1491
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1492
|
+
for (const target of this.selectionChangeTargets) {
|
|
1493
|
+
target.removeEventListener("selectionchange", this.onSelectionChange);
|
|
1351
1494
|
}
|
|
1495
|
+
this.selectionChangeTargets.length = 0;
|
|
1352
1496
|
for (const ext of this.extensions) {
|
|
1353
1497
|
(_a = ext.onDestroy) === null || _a === void 0 ? void 0 : _a.call(ext, this);
|
|
1354
1498
|
}
|
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.
|
|
4
|
+
version: 0.2.5
|
|
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.
|
|
99
|
+
documentation_uri: https://rubydoc.info/gems/smeditor/0.2.5
|
|
100
100
|
rubygems_mfa_required: 'true'
|
|
101
101
|
rdoc_options: []
|
|
102
102
|
require_paths:
|