Skip to content

Commit 19215f3

Browse files
authored
feat(froala): rulerVerticalGutter — reserve the vertical ruler column up front (#24)
Field report: with the horizontal ruler reading ~820px, enabling the vertical ruler narrowed the content to ~800px. That shift is geometric truth — the 23px strip has to take its space from somewhere, and the horizontal ruler correctly re-reads the narrower width — but the reflow on toggle is jarring. New opt-in option (default false), scrollbar-gutter: stable style: - rulerVerticalGutter: true creates the vwrap/strip column at init with visibility:hidden, so the 23px is spent once up front and toggling the vertical ruler never moves the content. - hide switches visibility (gutter kept) instead of display:none; the horizontal ruler offset applies whenever the gutter exists. - Default path unchanged: hidden strip still reclaims its width. Docs: option documented in both root READMEs + froala package READMEs (en/ko), demo site enables it on the Froala tab, CDN pins @0.12 -> @0.13. Verified: 91 unit tests green (3 new — gutter reserved up front, no reflow across toggles, default still reclaims); real-browser check on the built demo — horizontal ruler 794px and text left edge unchanged (0px moved) across show/hide.
1 parent 82d660a commit 19215f3

14 files changed

Lines changed: 144 additions & 27 deletions

File tree

CHANGELOG.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,23 @@ with a shared version number.
99

1010
## [Unreleased]
1111

12+
## [0.13.0] - 2026-08-06
13+
14+
### Added
15+
16+
- `@devslab/editor-ruler-froala`**`rulerVerticalGutter` option** (default
17+
false): reserves the vertical ruler's 23px column from init, like CSS
18+
`scrollbar-gutter: stable`. Without it, showing the vertical ruler narrows
19+
the writable area by 23px (the strip has to take its space from somewhere,
20+
and the horizontal ruler correctly re-reads the narrower width — e.g.
21+
820 → ~797); with the gutter on, that space is always reserved, so toggling
22+
the vertical ruler never reflows the content. Prompted by a field report of
23+
the width shift — thank you!
24+
25+
### Changed
26+
27+
- README CDN pins bumped `@0.12``@0.13`.
28+
1229
## [0.12.3] - 2026-08-06
1330

1431
### Fixed
@@ -214,7 +231,8 @@ with a shared version number.
214231
records one Froala undo step per gesture. iife build exposes the
215232
`EditorRulerFroala` global (core bundled) for CDN use.
216233

217-
[Unreleased]: https://github.com/devslab-kr/editor-ruler/compare/v0.12.3...HEAD
234+
[Unreleased]: https://github.com/devslab-kr/editor-ruler/compare/v0.13.0...HEAD
235+
[0.13.0]: https://github.com/devslab-kr/editor-ruler/compare/v0.12.3...v0.13.0
218236
[0.12.3]: https://github.com/devslab-kr/editor-ruler/compare/v0.12.2...v0.12.3
219237
[0.12.2]: https://github.com/devslab-kr/editor-ruler/compare/v0.12.1...v0.12.2
220238
[0.12.1]: https://github.com/devslab-kr/editor-ruler/compare/v0.12.0...v0.12.1

README.ko.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ ruler.refresh(); // 선택/내용이 바뀔 때마다 호출
4444
iife 빌드가 전역 `EditorRuler`를 노출합니다:
4545

4646
```html
47-
<script src="https://cdn.jsdelivr.net/npm/@devslab/editor-ruler@0.12/dist/index.global.js"></script>
47+
<script src="https://cdn.jsdelivr.net/npm/@devslab/editor-ruler@0.13/dist/index.global.js"></script>
4848
<script>
4949
const ruler = EditorRuler.createRuler(mountElement, { /* 동일한 옵션 */ });
5050
</script>
@@ -56,8 +56,8 @@ Froala 어댑터도 동일합니다 — `@devslab/editor-ruler-froala/dist/index
5656

5757
| URL | 의미 |
5858
|---|---|
59-
| `@0.12.2` | 정확한 버전 고정 — 절대 안 바뀜, 캐시 최장 |
60-
| `@0.12` | `0.12.x` 최신 패치 — 버그픽스 자동 반영, 브레이킹 없음 (권장) |
59+
| `@0.13.0` | 정확한 버전 고정 — 절대 안 바뀜, 캐시 최장 |
60+
| `@0.13` | `0.13.x` 최신 패치 — 버그픽스 자동 반영, 브레이킹 없음 (권장) |
6161
| `@latest` (또는 버전 생략) | 항상 최신 릴리스 — 메이저 포함이라 브레이킹 체인지가 예고 없이 들어올 수 있음; jsDelivr가 별칭을 최대 12시간 캐시 |
6262

6363
## 빠른 시작 (Froala)
@@ -119,7 +119,7 @@ ClassicEditor.create(element, {
119119
- `rulerOptions`**권장 단일 버튼**: 줄자 아이콘 드롭다운 하나에 보이기/숨기기 · 세로 줄자 · 가이드 잠금 · 가이드 지우기 + cm / inch / px (활성 상태 체크 표시)
120120
- `toggleRuler` / `rulerUnit` — 핵심 기능을 버튼을 나눠 쓰고 싶을 때
121121

122-
Froala 옵션: `rulerVertical: true`면 초기화 시 세로 줄자 표시, `rulerGuides: false`면 가이드선 비활성.
122+
Froala 옵션: `rulerVertical: true`면 초기화 시 세로 줄자 표시, `rulerVerticalGutter: true`면 세로 줄자의 23px 자리를 처음부터 예약해(`scrollbar-gutter: stable`과 같은 발상) 토글해도 본문이 리플로우되지 않음, `rulerGuides: false`면 가이드선 비활성.
123123

124124
## 기능
125125

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ ruler.refresh(); // call whenever selection or content changes
4444
The iife build exposes an `EditorRuler` global:
4545

4646
```html
47-
<script src="https://cdn.jsdelivr.net/npm/@devslab/editor-ruler@0.12/dist/index.global.js"></script>
47+
<script src="https://cdn.jsdelivr.net/npm/@devslab/editor-ruler@0.13/dist/index.global.js"></script>
4848
<script>
4949
const ruler = EditorRuler.createRuler(mountElement, { /* same options */ });
5050
</script>
@@ -56,8 +56,8 @@ Version pinning options:
5656

5757
| URL | Meaning |
5858
|---|---|
59-
| `@0.12.2` | Exact version — never changes, cached longest |
60-
| `@0.12` | Latest `0.12.x` patch — bugfixes auto-applied, no breaking changes (recommended) |
59+
| `@0.13.0` | Exact version — never changes, cached longest |
60+
| `@0.13` | Latest `0.13.x` patch — bugfixes auto-applied, no breaking changes (recommended) |
6161
| `@latest` (or no version) | Always the newest release — majors included, so breaking changes can land without warning; jsDelivr caches the alias for up to 12h |
6262

6363
## Quick start (Froala)
@@ -119,7 +119,7 @@ Model attributes down-cast to plain inline CSS; one undo step per drag. See the
119119
- `rulerOptions`**recommended single button**: one ruler-icon dropdown holding Show/Hide, Vertical Ruler, Lock Guides, Clear Guides, plus cm / inch / px (active states checkmarked)
120120
- `toggleRuler` / `rulerUnit` — the same core functions as separate buttons, for hosts that prefer them split
121121

122-
Froala options: `rulerVertical: true` shows the vertical ruler on init; `rulerGuides: false` disables guide lines.
122+
Froala options: `rulerVertical: true` shows the vertical ruler on init; `rulerVerticalGutter: true` reserves the strip's 23px column from the start (like `scrollbar-gutter: stable`) so toggling the vertical ruler never reflows the content; `rulerGuides: false` disables guide lines.
123123

124124
## Features
125125

packages/editor-ruler-ckeditor5/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devslab/editor-ruler-ckeditor5",
3-
"version": "0.12.3",
3+
"version": "0.13.0",
44
"description": "CKEditor 5 plugin for @devslab/editor-ruler — a Word-like horizontal ruler with margins, first-line indent, and guide lines.",
55
"license": "Apache-2.0",
66
"author": "devslab",

packages/editor-ruler-froala/README.ko.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,14 @@ import { defineRulerPlugin } from '@devslab/editor-ruler-froala';
1717
defineRulerPlugin(FroalaEditor); // 에디터 인스턴스 생성 전에 1회
1818

1919
new FroalaEditor('#editor', {
20-
rulerEnabled: true, // 기본 true
21-
rulerUnit: 'cm', // 'cm' | 'in' | 'px'
22-
toolbarButtons: ['bold', 'italic', '|', 'toggleRuler'], // 툴바 토글 버튼 (선택)
20+
rulerEnabled: true, // 기본 true
21+
rulerUnit: 'cm', // 'cm' | 'in' | 'px'
22+
rulerVertical: false, // 초기화 시 세로 줄자 표시
23+
rulerVerticalGutter: false, // 세로 줄자의 23px 자리를 처음부터 예약
24+
// (scrollbar-gutter: stable 방식) — 토글해도
25+
// 본문이 리플로우되지 않음
26+
rulerGuides: true, // 가이드선 (줄자에서 드래그해 생성)
27+
toolbarButtons: ['bold', 'italic', '|', 'rulerOptions'], // 툴바 드롭다운 (선택)
2328
});
2429
```
2530

@@ -30,7 +35,7 @@ new FroalaEditor('#editor', {
3035
CDN 사용 — iife 빌드는 코어를 포함한 단일 파일로 전역 `EditorRulerFroala`를 노출합니다:
3136

3237
```html
33-
<script src="https://cdn.jsdelivr.net/npm/@devslab/editor-ruler-froala@0.12/dist/index.global.js"></script>
38+
<script src="https://cdn.jsdelivr.net/npm/@devslab/editor-ruler-froala@0.13/dist/index.global.js"></script>
3439
<script>
3540
EditorRulerFroala.defineRulerPlugin(FroalaEditor);
3641
</script>

packages/editor-ruler-froala/README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,14 @@ import { defineRulerPlugin } from '@devslab/editor-ruler-froala';
1717
defineRulerPlugin(FroalaEditor); // once, before creating editor instances
1818

1919
new FroalaEditor('#editor', {
20-
rulerEnabled: true, // default true
21-
rulerUnit: 'cm', // 'cm' | 'in' | 'px'
22-
toolbarButtons: ['bold', 'italic', '|', 'toggleRuler'], // optional toolbar toggle
20+
rulerEnabled: true, // default true
21+
rulerUnit: 'cm', // 'cm' | 'in' | 'px'
22+
rulerVertical: false, // show the vertical ruler on init
23+
rulerVerticalGutter: false, // reserve the vertical ruler's 23px column up
24+
// front (scrollbar-gutter: stable style), so
25+
// toggling it never reflows the content
26+
rulerGuides: true, // guide lines (drag out of a ruler)
27+
toolbarButtons: ['bold', 'italic', '|', 'rulerOptions'], // optional toolbar dropdown
2328
});
2429
```
2530

@@ -30,7 +35,7 @@ If you configure `pluginsEnabled` explicitly, add `'ruler'` to the list.
3035
CDN usage — the iife build bundles the core into one file and exposes `EditorRulerFroala`:
3136

3237
```html
33-
<script src="https://cdn.jsdelivr.net/npm/@devslab/editor-ruler-froala@0.12/dist/index.global.js"></script>
38+
<script src="https://cdn.jsdelivr.net/npm/@devslab/editor-ruler-froala@0.13/dist/index.global.js"></script>
3439
<script>
3540
EditorRulerFroala.defineRulerPlugin(FroalaEditor);
3641
</script>

packages/editor-ruler-froala/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devslab/editor-ruler-froala",
3-
"version": "0.12.3",
3+
"version": "0.13.0",
44
"description": "Froala WYSIWYG editor adapter for @devslab/editor-ruler — a Word-like horizontal ruler plugin.",
55
"license": "Apache-2.0",
66
"author": "devslab",

packages/editor-ruler-froala/src/index.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ export interface FroalaRulerOptions {
2323
rulerUnit?: RulerUnit;
2424
/** Show the vertical ruler strip on init. Default false. */
2525
rulerVertical?: boolean;
26+
/**
27+
* Reserve the vertical ruler's 23px column from the start (like CSS
28+
* `scrollbar-gutter: stable`), so toggling the vertical ruler never
29+
* reflows the content — hidden means an empty gutter, not reclaimed
30+
* width. Default false: the strip only takes space while visible.
31+
*/
32+
rulerVerticalGutter?: boolean;
2633
/** Enable guide lines (drag down from the ruler strip). Default true. */
2734
rulerGuides?: boolean;
2835
}
@@ -135,6 +142,7 @@ export function defineRulerPlugin(FroalaEditor: any, defineOptions: DefineRulerP
135142
rulerEnabled: true,
136143
rulerUnit: 'cm' satisfies RulerUnit,
137144
rulerVertical: false,
145+
rulerVerticalGutter: false,
138146
rulerGuides: true,
139147
rulerLanguage: null,
140148
},
@@ -367,9 +375,15 @@ export function defineRulerPlugin(FroalaEditor: any, defineOptions: DefineRulerP
367375
return parseFloat(win.getComputedStyle(el)[side]) || 0;
368376
}
369377

378+
function vGutter(): boolean {
379+
return editor.opts.rulerVerticalGutter === true;
380+
}
381+
370382
/** Extra x-offset the vertical ruler strip adds in front of the editor. */
371383
function vRulerOffset(): number {
372-
return vVisible ? 23 : 0; // 22px strip + 1px border
384+
// 22px strip + 1px border. In gutter mode the column is reserved even
385+
// while the strip is hidden, so the offset applies whenever it exists.
386+
return vVisible || (vGutter() && vwrap) ? 23 : 0;
373387
}
374388

375389
function alignMount(): void {
@@ -389,7 +403,7 @@ export function defineRulerPlugin(FroalaEditor: any, defineOptions: DefineRulerP
389403
return editor.$wp?.get?.(0) ?? editorEl().parentElement ?? editorEl();
390404
}
391405

392-
function showVRuler(): void {
406+
function ensureVWrap(): void {
393407
const el = editorEl();
394408
const doc = el.ownerDocument;
395409
if (!vwrap) {
@@ -421,14 +435,24 @@ export function defineRulerPlugin(FroalaEditor: any, defineOptions: DefineRulerP
421435
},
422436
});
423437
}
438+
}
439+
440+
function showVRuler(): void {
441+
ensureVWrap();
424442
vmount!.style.display = '';
443+
vmount!.style.visibility = '';
425444
vVisible = true;
426445
refresh();
427446
}
428447

429448
function hideVRuler(): void {
430449
if (!vmount) return;
431-
vmount.style.display = 'none';
450+
if (vGutter()) {
451+
// Keep the reserved column — hide the strip without reclaiming width.
452+
vmount.style.visibility = 'hidden';
453+
} else {
454+
vmount.style.display = 'none';
455+
}
432456
vVisible = false;
433457
refresh();
434458
}
@@ -536,7 +560,14 @@ export function defineRulerPlugin(FroalaEditor: any, defineOptions: DefineRulerP
536560
alignMount();
537561
visible = true;
538562

539-
if (editor.opts.rulerVertical === true) showVRuler();
563+
if (editor.opts.rulerVertical === true) {
564+
showVRuler();
565+
} else if (vGutter()) {
566+
// Reserve the gutter up front so a later toggle doesn't reflow content.
567+
ensureVWrap();
568+
vmount!.style.visibility = 'hidden';
569+
refresh();
570+
}
540571

541572
for (const event of ['mouseup', 'keyup', 'contentChanged', 'commands.after']) {
542573
editor.events?.on?.(event, refresh);

packages/editor-ruler-froala/test/plugin.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,60 @@ describe('defineRulerPlugin', () => {
236236
expect(container.contains(wrapper)).toBe(true); // wrapper restored to its old place
237237
});
238238

239+
it('rulerVerticalGutter reserves the strip column up front, hidden', () => {
240+
const FE = makeFroalaConstructor();
241+
defineRulerPlugin(FE);
242+
const { editor, container } = makeEditor({ rulerVerticalGutter: true });
243+
const api = FE.PLUGINS.ruler!(editor);
244+
api._init();
245+
246+
// The gutter exists from init even though the strip is not shown…
247+
expect(api.isVRulerVisible()).toBe(false);
248+
const vmount = container.querySelector('.edr-froala-vmount') as HTMLElement;
249+
expect(vmount).toBeTruthy();
250+
expect(vmount.style.visibility).toBe('hidden');
251+
expect(vmount.style.display).not.toBe('none');
252+
// …and the horizontal ruler is already offset past it.
253+
const mount = container.querySelector('.edr-froala-mount') as HTMLElement;
254+
expect(mount.style.paddingLeft).toBe('23px');
255+
});
256+
257+
it('rulerVerticalGutter keeps the column (no reflow) across vruler toggles', () => {
258+
const FE = makeFroalaConstructor();
259+
defineRulerPlugin(FE);
260+
const { editor, container } = makeEditor({ rulerVerticalGutter: true });
261+
const api = FE.PLUGINS.ruler!(editor);
262+
api._init();
263+
const vmount = container.querySelector('.edr-froala-vmount') as HTMLElement;
264+
const mount = container.querySelector('.edr-froala-mount') as HTMLElement;
265+
266+
api.toggleVRuler(); // show
267+
expect(api.isVRulerVisible()).toBe(true);
268+
expect(vmount.style.visibility).toBe('');
269+
expect(mount.style.paddingLeft).toBe('23px');
270+
271+
api.toggleVRuler(); // hide — gutter stays, width untouched
272+
expect(api.isVRulerVisible()).toBe(false);
273+
expect(vmount.style.visibility).toBe('hidden');
274+
expect(vmount.style.display).not.toBe('none');
275+
expect(mount.style.paddingLeft).toBe('23px');
276+
});
277+
278+
it('without the gutter option, hiding the vruler reclaims its width', () => {
279+
const FE = makeFroalaConstructor();
280+
defineRulerPlugin(FE);
281+
const { editor, container } = makeEditor({ rulerVertical: true });
282+
const api = FE.PLUGINS.ruler!(editor);
283+
api._init();
284+
const vmount = container.querySelector('.edr-froala-vmount') as HTMLElement;
285+
const mount = container.querySelector('.edr-froala-mount') as HTMLElement;
286+
expect(mount.style.paddingLeft).toBe('23px');
287+
288+
api.toggleVRuler(); // hide — strip removed from layout entirely
289+
expect(vmount.style.display).toBe('none');
290+
expect(mount.style.paddingLeft).toBe('0px');
291+
});
292+
239293
it('drags a horizontal guide from the horizontal ruler, vertical from the vertical', () => {
240294
const FE = makeFroalaConstructor();
241295
defineRulerPlugin(FE);

packages/editor-ruler-tiptap/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devslab/editor-ruler-tiptap",
3-
"version": "0.12.3",
3+
"version": "0.13.0",
44
"description": "Tiptap extension for @devslab/editor-ruler — a Word-like horizontal ruler with margins, first-line indent, and guide lines.",
55
"license": "Apache-2.0",
66
"author": "devslab",

0 commit comments

Comments
 (0)