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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84ee0bbaa2884f82336e20cf7962934d728d539ab71a68c87c865c09122d1100
4
- data.tar.gz: 80da9de751d692cb0398e80f9b3017324fbf848873dc6a2e73dfa3c85fe9c8fb
3
+ metadata.gz: e09edfe28f68de93439fdd0552894387afc89e6eadf4875fb164d1a45aaa7658
4
+ data.tar.gz: a76689f1341ffd28d5b9e2fa24bf3c6543c40a0b9f8cc83ccc3348c871c41de5
5
5
  SHA512:
6
- metadata.gz: 1a905c9df109f03de095aab6652d2eeedd1d42acffeb505a020872c6ffaf5ad88b4ea3cb7cdf5a518b449528b009c6eb53735ef3d61aa50ad6cc554f19114aeb
7
- data.tar.gz: eb385c65cbae2a229035f98a66950f160d7d301c475cf9976429eebda3e1cc32faf3acfe3206c3478689f9567d33d78a02554dae6d3fe58f01bd3b7a21dadd9c
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 || typeof document === "undefined")
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 = document.createRange();
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
- if (typeof document === "undefined")
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 || typeof document === "undefined")
1277
+ if (!this.element)
1271
1278
  return null;
1272
- const sel = document.getSelection();
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 || typeof document === "undefined") {
1300
+ if (!this.selection || !this.element) {
1294
1301
  return;
1295
1302
  }
1296
- const sel = document.getSelection();
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 = document.createRange();
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: 8em;
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module SMEditor
4
4
  module Rails
5
- VERSION = "0.2.1"
5
+ VERSION = "0.2.3"
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.1
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.1
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: