smeditor 0.2.1 → 0.2.3
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 +11 -0
- data/README.md +13 -1
- data/app/assets/javascripts/smeditor.js +41 -10
- 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: e09edfe28f68de93439fdd0552894387afc89e6eadf4875fb164d1a45aaa7658
|
|
4
|
+
data.tar.gz: a76689f1341ffd28d5b9e2fa24bf3c6543c40a0b9f8cc83ccc3348c871c41de5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 06f5e3c97c24a257a36d115e3ceebc42f0ab70060c9e5013d81807ac0d77af20a218ea6597972724e1df5492e26733eb382cedf0fbb638ecab90839f86d3d1ab
|
|
7
|
+
data.tar.gz: 36f25a97de694d48cff314de6bffef35bd6016d2960489601881ad35ef893ce580a3b4a3a4ba86c395e13bca62140b236170d985a6f3bc0f0ebcd19043aca43a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog — smeditor
|
|
2
2
|
|
|
3
|
+
## 0.2.3
|
|
4
|
+
|
|
5
|
+
- Added configurable responsive minimum editor heights for desktop, tablet, and mobile.
|
|
6
|
+
- Added per-field `min_height`, `tablet_min_height`, and `mobile_min_height` Rails helper options.
|
|
7
|
+
- Added matching global defaults in `SMEditor.configure`.
|
|
8
|
+
|
|
9
|
+
## 0.2.2 — 2026-09-09
|
|
10
|
+
|
|
11
|
+
- 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()`.
|
|
12
|
+
- Selection restore and caret coordinate ranges now use the editor element's owner document, keeping the DOM integration correct in isolated roots and embedded documents.
|
|
13
|
+
|
|
3
14
|
## 0.2.1 — 2026-09-09
|
|
4
15
|
|
|
5
16
|
- Rails editor now renders inside Shadow DOM so host application CSS cannot alter the SMEditor toolbar or editing surface.
|
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.
|
|
@@ -476,6 +476,15 @@ function bootMount(mount) {
|
|
|
476
476
|
const root = shadowRootFor(mount);
|
|
477
477
|
const shell = document.createElement("div");
|
|
478
478
|
shell.className = "smeditor";
|
|
479
|
+
const responsiveHeights = [
|
|
480
|
+
["--sme-editor-min-height", mount.dataset.smeditorMinHeight],
|
|
481
|
+
["--sme-editor-min-height-tablet", mount.dataset.smeditorTabletMinHeight],
|
|
482
|
+
["--sme-editor-min-height-mobile", mount.dataset.smeditorMobileMinHeight],
|
|
483
|
+
];
|
|
484
|
+
for (const [property, value] of responsiveHeights) {
|
|
485
|
+
if (value)
|
|
486
|
+
shell.style.setProperty(property, value);
|
|
487
|
+
}
|
|
479
488
|
const surface = document.createElement("div");
|
|
480
489
|
surface.className = "smeditor-editor";
|
|
481
490
|
surface.setAttribute("aria-label", mount.dataset.smeditorLabel || "Rich text editor");
|
|
@@ -763,14 +772,14 @@ class Editor {
|
|
|
763
772
|
*/
|
|
764
773
|
coordsAtPoint(point) {
|
|
765
774
|
var _a;
|
|
766
|
-
if (!this.element
|
|
775
|
+
if (!this.element)
|
|
767
776
|
return null;
|
|
768
777
|
const deep = (_a = this.options.deepSelection) !== null && _a !== void 0 ? _a : false;
|
|
769
778
|
const loc = domNodeFromPoint(point, this.element, deep);
|
|
770
779
|
if (!loc)
|
|
771
780
|
return null;
|
|
772
781
|
try {
|
|
773
|
-
const range =
|
|
782
|
+
const range = this.element.ownerDocument.createRange();
|
|
774
783
|
const offset = Math.min(loc.offset, lengthOf(loc.node));
|
|
775
784
|
range.setStart(loc.node, offset);
|
|
776
785
|
range.collapse(true);
|
|
@@ -1243,9 +1252,7 @@ class Editor {
|
|
|
1243
1252
|
var _a, _b;
|
|
1244
1253
|
if (this.updatingDOM || !this.element)
|
|
1245
1254
|
return;
|
|
1246
|
-
|
|
1247
|
-
return;
|
|
1248
|
-
const domSel = document.getSelection();
|
|
1255
|
+
const domSel = selectionForElement(this.element);
|
|
1249
1256
|
if (!domSel)
|
|
1250
1257
|
return;
|
|
1251
1258
|
if (!this.element.contains(domSel.anchorNode))
|
|
@@ -1267,9 +1274,9 @@ class Editor {
|
|
|
1267
1274
|
}
|
|
1268
1275
|
readSelectionFromDOM() {
|
|
1269
1276
|
var _a;
|
|
1270
|
-
if (!this.element
|
|
1277
|
+
if (!this.element)
|
|
1271
1278
|
return null;
|
|
1272
|
-
const sel =
|
|
1279
|
+
const sel = selectionForElement(this.element);
|
|
1273
1280
|
if (!sel || sel.rangeCount === 0)
|
|
1274
1281
|
return null;
|
|
1275
1282
|
const deep = (_a = this.options.deepSelection) !== null && _a !== void 0 ? _a : false;
|
|
@@ -1290,10 +1297,10 @@ class Editor {
|
|
|
1290
1297
|
}
|
|
1291
1298
|
applySelectionToDOM() {
|
|
1292
1299
|
var _a;
|
|
1293
|
-
if (!this.selection || !this.element
|
|
1300
|
+
if (!this.selection || !this.element) {
|
|
1294
1301
|
return;
|
|
1295
1302
|
}
|
|
1296
|
-
const sel =
|
|
1303
|
+
const sel = selectionForElement(this.element);
|
|
1297
1304
|
if (!sel)
|
|
1298
1305
|
return;
|
|
1299
1306
|
const deep = (_a = this.options.deepSelection) !== null && _a !== void 0 ? _a : false;
|
|
@@ -1302,7 +1309,7 @@ class Editor {
|
|
|
1302
1309
|
if (!anchorNode || !headNode)
|
|
1303
1310
|
return;
|
|
1304
1311
|
try {
|
|
1305
|
-
const range =
|
|
1312
|
+
const range = this.element.ownerDocument.createRange();
|
|
1306
1313
|
range.setStart(anchorNode.node, Math.min(anchorNode.offset, lengthOf(anchorNode.node)));
|
|
1307
1314
|
range.setEnd(headNode.node, Math.min(headNode.offset, lengthOf(headNode.node)));
|
|
1308
1315
|
sel.removeAllRanges();
|
|
@@ -1353,6 +1360,30 @@ class Editor {
|
|
|
1353
1360
|
// ============================================================================
|
|
1354
1361
|
// Helpers (module-private)
|
|
1355
1362
|
// ============================================================================
|
|
1363
|
+
/**
|
|
1364
|
+
* Return the Selection object that actually owns the editor caret.
|
|
1365
|
+
*
|
|
1366
|
+
* `document.getSelection()` is not sufficient for editors mounted inside an
|
|
1367
|
+
* open ShadowRoot: Chromium exposes a composed selection on the document whose
|
|
1368
|
+
* anchor can be the shadow host instead of the text node inside the editor.
|
|
1369
|
+
* Reading that selection produces a null/wrong editor position; the next DOM
|
|
1370
|
+
* render then loses the caret (most visibly when typing spaces).
|
|
1371
|
+
*
|
|
1372
|
+
* Both Document and Chromium's ShadowRoot expose getSelection(). Prefer the
|
|
1373
|
+
* editor's root and fall back to its owner document for browsers that do not
|
|
1374
|
+
* expose ShadowRoot#getSelection.
|
|
1375
|
+
*/
|
|
1376
|
+
function selectionForElement(element) {
|
|
1377
|
+
var _a, _b, _c, _d;
|
|
1378
|
+
const root = (_a = element.getRootNode) === null || _a === void 0 ? void 0 : _a.call(element);
|
|
1379
|
+
const rootedGetSelection = root === null || root === void 0 ? void 0 : root.getSelection;
|
|
1380
|
+
if (root && typeof rootedGetSelection === "function") {
|
|
1381
|
+
const selection = rootedGetSelection.call(root);
|
|
1382
|
+
if (selection)
|
|
1383
|
+
return selection;
|
|
1384
|
+
}
|
|
1385
|
+
return (_d = (_c = (_b = element.ownerDocument) === null || _b === void 0 ? void 0 : _b.getSelection) === null || _c === void 0 ? void 0 : _c.call(_b)) !== null && _d !== void 0 ? _d : null;
|
|
1386
|
+
}
|
|
1356
1387
|
/**
|
|
1357
1388
|
* Strip noise and unsafe elements from pasted HTML.
|
|
1358
1389
|
*
|
|
@@ -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.3
|
|
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.3
|
|
100
100
|
rubygems_mfa_required: 'true'
|
|
101
101
|
rdoc_options: []
|
|
102
102
|
require_paths:
|