smeditor 0.2.2 → 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 +12 -0
- data/README.md +13 -1
- data/app/assets/javascripts/smeditor.js +145 -4
- data/app/assets/stylesheets/smeditor.css +18 -1
- data/lib/generators/smeditor/templates/initializer.rb +6 -0
- data/lib/smeditor/rails/config.rb +10 -0
- data/lib/smeditor/rails/form_helper.rb +17 -1
- 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,17 @@
|
|
|
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
|
+
|
|
9
|
+
## 0.2.3
|
|
10
|
+
|
|
11
|
+
- Added configurable responsive minimum editor heights for desktop, tablet, and mobile.
|
|
12
|
+
- Added per-field `min_height`, `tablet_min_height`, and `mobile_min_height` Rails helper options.
|
|
13
|
+
- Added matching global defaults in `SMEditor.configure`.
|
|
14
|
+
|
|
3
15
|
## 0.2.2 — 2026-09-09
|
|
4
16
|
|
|
5
17
|
- Fixed caret jumps and reordered typing inside the Rails Shadow DOM editor. The core now resolves the live DOM Selection from the editor's own root instead of always using `document.getSelection()`.
|
data/README.md
CHANGED
|
@@ -42,6 +42,9 @@ SMEditor.configure do |config|
|
|
|
42
42
|
config.uploads = :none # or :active_storage
|
|
43
43
|
config.upload_path = "/smeditor/uploads"
|
|
44
44
|
config.default_kit = "starter" # or "full"
|
|
45
|
+
config.min_height = 320 # desktop, px when numeric
|
|
46
|
+
config.tablet_min_height = 280 # <= 1024px
|
|
47
|
+
config.mobile_min_height = 220 # <= 640px
|
|
45
48
|
config.sanitize_output = true
|
|
46
49
|
|
|
47
50
|
# true by default: the first editor field includes smeditor.js/css.
|
|
@@ -61,7 +64,10 @@ end
|
|
|
61
64
|
<%= form_with model: @article do |form| %>
|
|
62
65
|
<%= form.smeditor_editor :content,
|
|
63
66
|
kit: "full",
|
|
64
|
-
placeholder: "Write…"
|
|
67
|
+
placeholder: "Write…",
|
|
68
|
+
min_height: 420,
|
|
69
|
+
tablet_min_height: 340,
|
|
70
|
+
mobile_min_height: 260 %>
|
|
65
71
|
|
|
66
72
|
<%= form.submit %>
|
|
67
73
|
<% end %>
|
|
@@ -79,8 +85,14 @@ Available options:
|
|
|
79
85
|
- `class:`
|
|
80
86
|
- `label:` — editor ARIA label
|
|
81
87
|
- `upload_url:` — custom image upload endpoint
|
|
88
|
+
- `min_height:` — minimum height on desktop (> 1024px)
|
|
89
|
+
- `tablet_min_height:` — minimum height on tablet widths (641-1024px)
|
|
90
|
+
- `mobile_min_height:` — minimum height on phone widths (<= 640px)
|
|
82
91
|
- `include_assets: false` — skip automatic asset tags for this field
|
|
83
92
|
|
|
93
|
+
Height options accept numbers (treated as pixels) or CSS lengths such as `24rem`,
|
|
94
|
+
`45vh`, or `60dvh`. Per-field values override the initializer defaults.
|
|
95
|
+
|
|
84
96
|
### Assets in the layout (optional)
|
|
85
97
|
|
|
86
98
|
By default the first `smeditor_editor` call includes the packaged assets once.
|
|
@@ -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 = "";
|
|
@@ -476,6 +590,15 @@ function bootMount(mount) {
|
|
|
476
590
|
const root = shadowRootFor(mount);
|
|
477
591
|
const shell = document.createElement("div");
|
|
478
592
|
shell.className = "smeditor";
|
|
593
|
+
const responsiveHeights = [
|
|
594
|
+
["--sme-editor-min-height", mount.dataset.smeditorMinHeight],
|
|
595
|
+
["--sme-editor-min-height-tablet", mount.dataset.smeditorTabletMinHeight],
|
|
596
|
+
["--sme-editor-min-height-mobile", mount.dataset.smeditorMobileMinHeight],
|
|
597
|
+
];
|
|
598
|
+
for (const [property, value] of responsiveHeights) {
|
|
599
|
+
if (value)
|
|
600
|
+
shell.style.setProperty(property, value);
|
|
601
|
+
}
|
|
479
602
|
const surface = document.createElement("div");
|
|
480
603
|
surface.className = "smeditor-editor";
|
|
481
604
|
surface.setAttribute("aria-label", mount.dataset.smeditorLabel || "Rich text editor");
|
|
@@ -495,11 +618,16 @@ function bootMount(mount) {
|
|
|
495
618
|
});
|
|
496
619
|
const toolbar = buildToolbar(editor, kit, uploadUrl);
|
|
497
620
|
shell.insertBefore(toolbar, surface);
|
|
621
|
+
const bubbleMenu = buildBubbleMenu(editor, surface);
|
|
622
|
+
root.appendChild(bubbleMenu);
|
|
498
623
|
input.smeditorInstance = editor;
|
|
499
624
|
mount.smeditorInstance = editor;
|
|
500
625
|
mount.dataset.smeditorBooted = "1";
|
|
501
626
|
instances.set(mount, editor);
|
|
502
|
-
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
|
+
]);
|
|
503
631
|
}
|
|
504
632
|
function boot(root = document) {
|
|
505
633
|
root.querySelectorAll("[data-smeditor]").forEach(bootMount);
|
|
@@ -681,6 +809,7 @@ class Editor {
|
|
|
681
809
|
this.onDomFocus = () => this.emit("focus", { editor: this });
|
|
682
810
|
this.onDomBlur = () => this.emit("blur", { editor: this });
|
|
683
811
|
this.onSelectionChange = () => this.syncSelectionFromDOM();
|
|
812
|
+
this.selectionChangeTargets = [];
|
|
684
813
|
this.onCompositionStart = () => {
|
|
685
814
|
this.composing = true;
|
|
686
815
|
};
|
|
@@ -988,6 +1117,7 @@ class Editor {
|
|
|
988
1117
|
this.attachToElement();
|
|
989
1118
|
}
|
|
990
1119
|
attachToElement() {
|
|
1120
|
+
var _a;
|
|
991
1121
|
const el = this.element;
|
|
992
1122
|
el.setAttribute("contenteditable", String(this.options.editable !== false));
|
|
993
1123
|
el.classList.add("smeditor-editor");
|
|
@@ -1000,7 +1130,17 @@ class Editor {
|
|
|
1000
1130
|
el.addEventListener("compositionstart", this.onCompositionStart);
|
|
1001
1131
|
el.addEventListener("compositionend", this.onCompositionEnd);
|
|
1002
1132
|
if (typeof document !== "undefined") {
|
|
1003
|
-
|
|
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
|
+
}
|
|
1004
1144
|
}
|
|
1005
1145
|
this.renderToDOM();
|
|
1006
1146
|
}
|
|
@@ -1337,9 +1477,10 @@ class Editor {
|
|
|
1337
1477
|
this.element.removeEventListener("compositionstart", this.onCompositionStart);
|
|
1338
1478
|
this.element.removeEventListener("compositionend", this.onCompositionEnd);
|
|
1339
1479
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1480
|
+
for (const target of this.selectionChangeTargets) {
|
|
1481
|
+
target.removeEventListener("selectionchange", this.onSelectionChange);
|
|
1342
1482
|
}
|
|
1483
|
+
this.selectionChangeTargets.length = 0;
|
|
1343
1484
|
for (const ext of this.extensions) {
|
|
1344
1485
|
(_a = ext.onDestroy) === null || _a === void 0 ? void 0 : _a.call(ext, this);
|
|
1345
1486
|
}
|
|
@@ -79,6 +79,11 @@
|
|
|
79
79
|
|
|
80
80
|
/* Indent step (block-level indentation) */
|
|
81
81
|
--sme-indent-step: 24px;
|
|
82
|
+
|
|
83
|
+
/* Responsive editor surface height. Rails can override these per field. */
|
|
84
|
+
--sme-editor-min-height: 320px;
|
|
85
|
+
--sme-editor-min-height-tablet: 280px;
|
|
86
|
+
--sme-editor-min-height-mobile: 220px;
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
/* ---------------------------------------------------------------------------
|
|
@@ -366,7 +371,7 @@
|
|
|
366
371
|
padding: var(--sme-space-4);
|
|
367
372
|
font-size: var(--sme-text-base);
|
|
368
373
|
line-height: var(--sme-leading);
|
|
369
|
-
min-height:
|
|
374
|
+
min-height: var(--sme-editor-min-height, 320px);
|
|
370
375
|
outline: none;
|
|
371
376
|
}
|
|
372
377
|
|
|
@@ -1186,7 +1191,19 @@
|
|
|
1186
1191
|
font-size: 11px;
|
|
1187
1192
|
}
|
|
1188
1193
|
|
|
1194
|
+
@media (max-width: 1024px) {
|
|
1195
|
+
.smeditor-content,
|
|
1196
|
+
.smeditor-editor {
|
|
1197
|
+
min-height: var(--sme-editor-min-height-tablet, 280px);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1189
1201
|
@media (max-width: 640px) {
|
|
1202
|
+
.smeditor-content,
|
|
1203
|
+
.smeditor-editor {
|
|
1204
|
+
min-height: var(--sme-editor-min-height-mobile, 220px);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1190
1207
|
.smeditor-toolbar {
|
|
1191
1208
|
gap: 4px;
|
|
1192
1209
|
padding: var(--sme-space-2);
|
|
@@ -14,6 +14,12 @@ SMEditor.configure do |config|
|
|
|
14
14
|
# "starter" gives the MVP toolbar; "full" enables the larger extension set.
|
|
15
15
|
config.default_kit = "starter"
|
|
16
16
|
|
|
17
|
+
# Responsive minimum editor height. Numeric values are pixels. You can also
|
|
18
|
+
# use CSS lengths such as "24rem" or "45vh".
|
|
19
|
+
config.min_height = 320
|
|
20
|
+
config.tablet_min_height = 280
|
|
21
|
+
config.mobile_min_height = 220
|
|
22
|
+
|
|
17
23
|
# Stored editor HTML is sanitized before public rendering.
|
|
18
24
|
config.sanitize_output = true
|
|
19
25
|
|
|
@@ -23,6 +23,13 @@ module SMEditor
|
|
|
23
23
|
# Include the packaged JS/CSS automatically with the first editor field.
|
|
24
24
|
# Set false when the host app calls `smeditor_assets` in its layout.
|
|
25
25
|
attr_accessor :auto_include_assets
|
|
26
|
+
# Default minimum editor heights. They are responsive by viewport width:
|
|
27
|
+
# desktop (> 1024px), tablet (641-1024px), and mobile (<= 640px).
|
|
28
|
+
# Numeric values are interpreted as pixels; CSS lengths such as "24rem"
|
|
29
|
+
# and "45vh" are accepted too.
|
|
30
|
+
attr_accessor :min_height
|
|
31
|
+
attr_accessor :tablet_min_height
|
|
32
|
+
attr_accessor :mobile_min_height
|
|
26
33
|
# Hard ceiling on an uploaded file, in bytes. The endpoint is reachable
|
|
27
34
|
# by anyone who can reach the host app unless the controller is
|
|
28
35
|
# subclassed with authentication, so it refuses anything larger.
|
|
@@ -42,6 +49,9 @@ module SMEditor
|
|
|
42
49
|
@default_kit = "starter"
|
|
43
50
|
@upload_path = "/smeditor/uploads"
|
|
44
51
|
@auto_include_assets = true
|
|
52
|
+
@min_height = "320px"
|
|
53
|
+
@tablet_min_height = "280px"
|
|
54
|
+
@mobile_min_height = "220px"
|
|
45
55
|
@max_upload_size = 10 * 1024 * 1024
|
|
46
56
|
@allowed_upload_types = %w[
|
|
47
57
|
image/png image/jpeg image/gif image/webp image/avif image/bmp
|
|
@@ -23,7 +23,8 @@ module SMEditor
|
|
|
23
23
|
# form - a Rails FormBuilder
|
|
24
24
|
# method - the model attribute (stored as an HTML string)
|
|
25
25
|
# options - :kit ("starter" | "full"), :class, :placeholder,
|
|
26
|
-
# :upload_url, :label, :include_assets
|
|
26
|
+
# :upload_url, :label, :include_assets, :min_height,
|
|
27
|
+
# :tablet_min_height, :mobile_min_height
|
|
27
28
|
def smeditor_editor(form, method, options = {})
|
|
28
29
|
object = form.object
|
|
29
30
|
current = object.respond_to?(method) ? object.public_send(method) : nil
|
|
@@ -31,6 +32,9 @@ module SMEditor
|
|
|
31
32
|
upload_url = options.fetch(:upload_url, smeditor_upload_url)
|
|
32
33
|
include_assets = options.fetch(:include_assets, SMEditor.config.auto_include_assets)
|
|
33
34
|
stylesheet_url = asset_path("smeditor.css")
|
|
35
|
+
min_height = smeditor_css_length(options.fetch(:min_height, SMEditor.config.min_height), :min_height)
|
|
36
|
+
tablet_min_height = smeditor_css_length(options.fetch(:tablet_min_height, SMEditor.config.tablet_min_height), :tablet_min_height)
|
|
37
|
+
mobile_min_height = smeditor_css_length(options.fetch(:mobile_min_height, SMEditor.config.mobile_min_height), :mobile_min_height)
|
|
34
38
|
|
|
35
39
|
hidden = form.hidden_field(
|
|
36
40
|
method,
|
|
@@ -49,6 +53,9 @@ module SMEditor
|
|
|
49
53
|
smeditor_upload_url: upload_url,
|
|
50
54
|
smeditor_label: options[:label],
|
|
51
55
|
smeditor_stylesheet: stylesheet_url,
|
|
56
|
+
smeditor_min_height: min_height,
|
|
57
|
+
smeditor_tablet_min_height: tablet_min_height,
|
|
58
|
+
smeditor_mobile_min_height: mobile_min_height,
|
|
52
59
|
}.compact,
|
|
53
60
|
)
|
|
54
61
|
|
|
@@ -60,6 +67,15 @@ module SMEditor
|
|
|
60
67
|
|
|
61
68
|
private
|
|
62
69
|
|
|
70
|
+
CSS_LENGTH = %r{\A(?:0|(?:\d+(?:\.\d+)?)(?:px|rem|em|vh|dvh|svh|vw|%))\z}.freeze
|
|
71
|
+
|
|
72
|
+
def smeditor_css_length(value, option_name)
|
|
73
|
+
normalized = value.is_a?(Numeric) ? "#{value}px" : value.to_s.strip
|
|
74
|
+
return normalized if normalized.match?(CSS_LENGTH)
|
|
75
|
+
|
|
76
|
+
raise ArgumentError, "#{option_name} must be a number (pixels) or a CSS length such as 320px, 24rem, or 45vh"
|
|
77
|
+
end
|
|
78
|
+
|
|
63
79
|
def smeditor_upload_url
|
|
64
80
|
SMEditor.config.upload_path if SMEditor.config.uploads == :active_storage
|
|
65
81
|
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.
|
|
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:
|