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 +4 -4
- data/CHANGELOG.md +6 -0
- data/app/assets/javascripts/smeditor.js +136 -4
- 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: 3edf61aad6834f2726ef15563ef798a7d71ae7758868f016fcd82af1b3521e39
|
|
4
|
+
data.tar.gz: 4ef021f5c72395a43f3f6dac47e5694c425bb4051691830f12ff83425f7e40e6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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, [
|
|
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
|
-
|
|
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
|
-
|
|
1350
|
-
|
|
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
|
}
|
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.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.
|
|
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:
|