Skip to content

Commit 89e535d

Browse files
committed
feat: add icon scaling support and update changelog for version 4.11.4
1 parent b5036b7 commit 89e535d

11 files changed

Lines changed: 236 additions & 42 deletions

File tree

CHANGELOG.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,47 @@
99
> - :house: [Internal]
1010
> - :nail_care: [Polish]
1111

12+
## 4.11.4
13+
14+
#### :boom: Breaking Change
15+
16+
- `.jodit-icon` `transform-origin` changed from `0 0 !important` to `var(--jd-icon-transform-origin)` (default `center`), and the `!important` flag was removed. If your layout depends on the old top-left origin, restore it via CSS:
17+
```css
18+
:root {
19+
--jd-icon-transform-origin: 0 0 !important;
20+
}
21+
```
22+
23+
#### :rocket: New Feature
24+
25+
- `IUIIconState` now supports `scale` property — when set, applies `transform: scale(...)` to the SVG icon element, overriding the CSS variable
26+
- `IControlType.icon` now accepts `string | IUIIconState` — allows setting icon name, fill, iconURL, and scale directly from toolbar button config
27+
28+
Per-button scale example:
29+
```javascript
30+
Jodit.make('#editor', {
31+
buttons: Jodit.atom([
32+
'bold',
33+
{
34+
name: 'big-italic',
35+
icon: { name: 'italic', fill: '', iconURL: '', scale: 1.5 },
36+
tooltip: 'Italic (large icon)'
37+
},
38+
'underline'
39+
])
40+
});
41+
```
42+
43+
- CSS custom properties `--jd-icon-transform-origin` and `--jd-icon-transform-scale` for global icon scaling. Override them to resize all editor icons at once. Per-button `scale` in `IUIIconState` takes priority over the CSS variable.
44+
45+
Global scale override via CSS:
46+
```css
47+
:root {
48+
--jd-icon-transform-scale: 1.3;
49+
--jd-icon-transform-origin: center;
50+
}
51+
```
52+
1253
## 4.11.2
1354

1455
#### :boom: Breaking Change

docs/security.md

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ Jodit.make('#editor', {
112112
```javascript
113113
Jodit.make('#editor', {
114114
cleanHTML: {
115-
allowTags: {
115+
allowTags: Jodit.atom({
116116
p: true, // allow <p> with any attributes
117117
a: { href: true }, // allow <a> only with href
118118
strong: true,
@@ -127,17 +127,23 @@ Jodit.make('#editor', {
127127
width: true,
128128
height: true
129129
}
130-
}
130+
})
131131
}
132132
});
133133
```
134134

135+
{% note info %}
136+
137+
Wrap object and array options with `Jodit.atom()`. Without it, Jodit deep-merges your value with the default config instead of replacing it. For example, `allowTags: { p: true }` without `Jodit.atom()` will be merged with the default value, not override it.
138+
139+
{% endnote %}
140+
135141
**Pin to an exact value:**
136142

137143
```javascript
138-
allowTags: {
144+
allowTags: Jodit.atom({
139145
img: { src: '/images/logo.png' } // only src="/images/logo.png" survives
140-
}
146+
})
141147
```
142148

143149
### denyTags -- blacklist mode
@@ -161,14 +167,20 @@ Inline `style` attributes can be used for CSS injection attacks (e.g., data exfi
161167
```javascript
162168
Jodit.make('#editor', {
163169
cleanHTML: {
164-
allowedStyles: {
170+
allowedStyles: Jodit.atom({
165171
'*': ['color', 'background-color', 'font-size', 'text-align', 'font-weight'],
166172
img: ['width', 'height']
167-
}
173+
})
168174
}
169175
});
170176
```
171177

178+
{% note info %}
179+
180+
Wrap `allowedStyles` with `Jodit.atom()` to replace the default value entirely. Without it, Jodit deep-merges your object with the defaults.
181+
182+
{% endnote %}
183+
172184
When set, any CSS property not in the whitelist is stripped. Supports global (`*`) and tag-specific rules.
173185

174186
### sanitizer -- external sanitizer hook
@@ -206,11 +218,17 @@ You can disable individual filters if they conflict with your use case:
206218
```javascript
207219
Jodit.make('#editor', {
208220
cleanHTML: {
209-
disableCleanFilter: new Set(['replaceOldTags']) // keep <i> and <b> as-is
221+
disableCleanFilter: Jodit.atom(new Set(['replaceOldTags'])) // keep <i> and <b> as-is
210222
}
211223
});
212224
```
213225

226+
{% note info %}
227+
228+
Wrap `Set` and other collection options with `Jodit.atom()` so Jodit replaces the value instead of merging it with the default.
229+
230+
{% endnote %}
231+
214232
Available filter names: `tryRemoveNode`, `allowAttributes`, `sanitizeAttributes`, `sanitizeStyles`, `replaceOldTags`, `fillEmptyParagraph`, `removeEmptyTextNode`, `removeInvTextNodes`, `safeLinksTarget`, `sandboxIframesInContent`, `convertUnsafeEmbeds`.
215233

216234
---
@@ -242,17 +260,23 @@ Jodit.make('#editor', {
242260
defaultActionOnPaste: 'insert_as_html',
243261

244262
// Tags preserved even in "insert only text" mode
245-
pasteExcludeStripTags: ['br', 'hr'],
263+
pasteExcludeStripTags: Jodit.atom(['br', 'hr']),
246264

247265
// Available options in the paste dialog
248-
pasteHTMLActionList: [
266+
pasteHTMLActionList: Jodit.atom([
249267
{ value: 'insert_as_html', text: 'Keep' },
250268
{ value: 'insert_as_text', text: 'Insert as Text' },
251269
{ value: 'insert_only_text', text: 'Insert only Text' }
252-
]
270+
])
253271
});
254272
```
255273

274+
{% note info %}
275+
276+
Wrap array options like `pasteExcludeStripTags` and `pasteHTMLActionList` with `Jodit.atom()`. Without it, Jodit deep-merges your array with the default instead of replacing it.
277+
278+
{% endnote %}
279+
256280
### For maximum safety on paste
257281

258282
```javascript
@@ -288,11 +312,11 @@ Jodit.make('#editor', {
288312
// Default action for Word content
289313
defaultActionOnPasteFromWord: 'insert_as_text', // cleanFromWord
290314

291-
pasteFromWordActionList: [
315+
pasteFromWordActionList: Jodit.atom([
292316
{ value: 'insert_as_html', text: 'Keep' },
293317
{ value: 'insert_as_text', text: 'Clean' },
294318
{ value: 'insert_only_text', text: 'Insert only Text' }
295-
]
319+
])
296320
});
297321
```
298322

@@ -379,21 +403,27 @@ To avoid whitelisting external CDNs, host the libraries yourself and override th
379403

380404
```javascript
381405
Jodit.make('#editor', {
382-
sourceEditorCDNUrlsJS: ['/vendor/ace/ace.js'],
383-
beautifyHTMLCDNUrlsJS: [
406+
sourceEditorCDNUrlsJS: Jodit.atom(['/vendor/ace/ace.js']),
407+
beautifyHTMLCDNUrlsJS: Jodit.atom([
384408
'/vendor/js-beautify/beautify.min.js',
385409
'/vendor/js-beautify/beautify-html.min.js'
386-
],
410+
]),
387411
// Pro only: pasteCode plugin
388412
pasteCode: {
389-
highlightLib: {
413+
highlightLib: Jodit.atom({
390414
js: ['/vendor/prism/prism.min.js'],
391415
css: ['/vendor/prism/prism.min.css']
392-
}
416+
})
393417
}
394418
});
395419
```
396420

421+
{% note info %}
422+
423+
Wrap array and object options with `Jodit.atom()` so Jodit replaces the default CDN URLs instead of merging with them.
424+
425+
{% endnote %}
426+
397427
Then your CSP needs only `'self'`:
398428

399429
```http
@@ -413,7 +443,7 @@ Alternatively, disable these features entirely if you don't need them:
413443
Jodit.make('#editor', {
414444
sourceEditor: 'area', // plain textarea instead of Ace
415445
beautifyHTML: false,
416-
disablePlugins: ['pasteCode'] // Pro only
446+
disablePlugins: Jodit.atom(['pasteCode']) // Pro only
417447
});
418448
```
419449

src/core/ui/button/button/button.test.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,62 @@ describe('Test UIButton', () => {
1212
editor = getJodit();
1313
});
1414

15+
describe('icon scale', () => {
16+
it('should apply transform scale to icon SVG when scale is set', () => {
17+
const button = new UIButton(editor);
18+
button.state.icon = {
19+
name: 'bold',
20+
fill: '',
21+
iconURL: '',
22+
scale: 1.5
23+
};
24+
25+
const svg = button.container.querySelector('.jodit-icon');
26+
expect(svg).is.not.null;
27+
expect(svg.style.transform).eq('scale(1.5)');
28+
});
29+
30+
it('should not set transform when scale is undefined', () => {
31+
const button = new UIButton(editor);
32+
button.state.icon = {
33+
name: 'bold',
34+
fill: '',
35+
iconURL: '',
36+
scale: undefined
37+
};
38+
39+
const svg = button.container.querySelector('.jodit-icon');
40+
expect(svg).is.not.null;
41+
expect(svg.style.transform).eq('');
42+
});
43+
44+
it('should reactively update icon when scale changes', async () => {
45+
const button = new UIButton(editor);
46+
button.state.icon = {
47+
name: 'bold',
48+
fill: '',
49+
iconURL: '',
50+
scale: undefined
51+
};
52+
53+
let svg = button.container.querySelector('.jodit-icon');
54+
expect(svg.style.transform).eq('');
55+
56+
button.state.icon = {
57+
name: 'bold',
58+
fill: '',
59+
iconURL: '',
60+
scale: 2
61+
};
62+
63+
await editor.async.requestIdlePromise();
64+
65+
svg = button.container.querySelector('.jodit-icon');
66+
expect(svg).is.not.null;
67+
expect(svg.style.transform).eq('scale(2)');
68+
});
69+
});
70+
1571
describe('aria-label', () => {
1672
it('should set aria-label from tooltip when text is empty', () => {
1773
const button = new UIButton(editor);

src/core/ui/button/button/button.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ export const UIButtonState = (): IUIButtonState => ({
4747
icon: {
4848
name: 'empty',
4949
fill: '',
50-
iconURL: ''
50+
iconURL: '',
51+
scale: undefined
5152
},
5253

5354
tooltip: '',

src/core/ui/icon.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,15 @@ export class Icon {
7575

7676
let iconElement: CanUndef<HTMLElement>;
7777

78-
const { name, iconURL, fill } = icon;
78+
const { name, iconURL, fill, scale } = icon;
7979
const clearName = name.replace(/[^a-zA-Z0-9]/g, '_');
8080

8181
let iconFromEvent: CanUndef<string>;
8282
if (!/<svg/.test(name)) {
8383
iconFromEvent = jodit.o.getIcon?.(name, clearName);
8484
}
8585

86-
const cacheKey = `${name}${iconURL}${fill}${iconFromEvent ?? ''}`;
86+
const cacheKey = `${name}${iconURL}${fill}${scale ?? ''}${iconFromEvent ?? ''}`;
8787

8888
if (jodit.o.cache && this.__cache.has(cacheKey)) {
8989
return this.__cache.get(cacheKey)?.cloneNode(true);
@@ -117,6 +117,11 @@ export class Icon {
117117
if (iconElement) {
118118
iconElement.classList.add('jodit-icon');
119119
iconElement.style.fill = fill;
120+
121+
if (scale != null) {
122+
iconElement.style.transform = `scale(${scale})`;
123+
}
124+
120125
jodit.o.cache &&
121126
this.__cache.set(
122127
cacheKey,

src/modules/toolbar/button/button.ts

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,19 @@ export class ToolbarButton<T extends IViewBased = IViewBased>
274274

275275
state.name = ctr.name;
276276

277+
this.__initIconFromControl();
278+
279+
if (ctr.tooltip) {
280+
state.tooltip = isFunction(ctr.tooltip)
281+
? ctr.tooltip(this.j, ctr, this)
282+
: ctr.tooltip;
283+
}
284+
285+
state.hasTrigger = Boolean(ctr.list || (ctr.popup && ctr.exec));
286+
}
287+
288+
private __initIconFromControl(): void {
289+
const { control: ctr, state } = this;
277290
const { textIcons } = this.j.o;
278291

279292
if (
@@ -283,29 +296,30 @@ export class ToolbarButton<T extends IViewBased = IViewBased>
283296
) {
284297
state.icon = UIButtonState().icon;
285298
state.text = ctr.text || ctr.name;
286-
} else {
287-
if (ctr.iconURL) {
288-
state.icon.iconURL = ctr.iconURL;
289-
} else {
290-
const name = ctr.icon || ctr.name;
291-
state.icon.name =
292-
Icon.exists(name) || this.j.o.extraIcons?.[name]
293-
? name
294-
: '';
295-
}
299+
return;
300+
}
296301

297-
if (!ctr.iconURL && !state.icon.name) {
298-
state.text = ctr.text || ctr.name;
299-
}
302+
if (!isString(ctr.icon) && ctr.icon != null) {
303+
state.icon = {
304+
name: ctr.icon.name || ctr.name,
305+
iconURL: ctr.icon.iconURL || '',
306+
fill: ctr.icon.fill || '',
307+
scale: ctr.icon.scale
308+
};
309+
return;
300310
}
301311

302-
if (ctr.tooltip) {
303-
state.tooltip = isFunction(ctr.tooltip)
304-
? ctr.tooltip(this.j, ctr, this)
305-
: ctr.tooltip;
312+
if (ctr.iconURL) {
313+
state.icon.iconURL = ctr.iconURL;
314+
} else {
315+
const name = ctr.icon || ctr.name;
316+
state.icon.name =
317+
Icon.exists(name) || this.j.o.extraIcons?.[name] ? name : '';
306318
}
307319

308-
state.hasTrigger = Boolean(ctr.list || (ctr.popup && ctr.exec));
320+
if (!ctr.iconURL && !state.icon.name) {
321+
state.text = ctr.text || ctr.name;
322+
}
309323
}
310324

311325
/**

0 commit comments

Comments
 (0)