fabric 7.0.0 → 7.2.0

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.
Files changed (323) hide show
  1. package/.husky/pre-commit +1 -0
  2. package/CHANGELOG.md +21 -0
  3. package/dist/extensions/cropping_controls/croppingControls.d.ts +16 -0
  4. package/dist/extensions/cropping_controls/croppingControls.d.ts.map +1 -0
  5. package/dist/extensions/cropping_controls/croppingHandlers.d.ts +39 -0
  6. package/dist/extensions/cropping_controls/croppingHandlers.d.ts.map +1 -0
  7. package/dist/extensions/cropping_controls/enterCropMode.d.ts +7 -0
  8. package/dist/extensions/cropping_controls/enterCropMode.d.ts.map +1 -0
  9. package/dist/extensions/cropping_controls/renderCornerControl.d.ts +14 -0
  10. package/dist/extensions/cropping_controls/renderCornerControl.d.ts.map +1 -0
  11. package/dist/extensions/index.d.ts +3 -0
  12. package/dist/extensions/index.d.ts.map +1 -1
  13. package/dist/fabric.d.ts +1 -0
  14. package/dist/fabric.d.ts.map +1 -1
  15. package/dist/index.js +628 -537
  16. package/dist/index.js.map +1 -1
  17. package/dist/index.min.js +1 -1
  18. package/dist/index.min.js.map +1 -1
  19. package/dist/index.min.mjs +1 -1
  20. package/dist/index.min.mjs.map +1 -1
  21. package/dist/index.mjs +628 -537
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/index.node.cjs +628 -537
  24. package/dist/index.node.cjs.map +1 -1
  25. package/dist/index.node.mjs +628 -537
  26. package/dist/index.node.mjs.map +1 -1
  27. package/dist/package.json.min.mjs +1 -1
  28. package/dist/package.json.mjs +1 -1
  29. package/dist/src/EventTypeDefs.d.ts +5 -0
  30. package/dist/src/EventTypeDefs.d.ts.map +1 -1
  31. package/dist/src/Pattern/Pattern.d.ts.map +1 -1
  32. package/dist/src/Pattern/Pattern.min.mjs +1 -1
  33. package/dist/src/Pattern/Pattern.min.mjs.map +1 -1
  34. package/dist/src/Pattern/Pattern.mjs +2 -1
  35. package/dist/src/Pattern/Pattern.mjs.map +1 -1
  36. package/dist/src/Shadow.d.ts +1 -1
  37. package/dist/src/Shadow.d.ts.map +1 -1
  38. package/dist/src/Shadow.min.mjs +1 -1
  39. package/dist/src/Shadow.min.mjs.map +1 -1
  40. package/dist/src/Shadow.mjs +5 -4
  41. package/dist/src/Shadow.mjs.map +1 -1
  42. package/dist/src/canvas/CanvasOptions.d.ts.map +1 -1
  43. package/dist/src/canvas/CanvasOptions.min.mjs.map +1 -1
  44. package/dist/src/canvas/CanvasOptions.mjs.map +1 -1
  45. package/dist/src/canvas/SelectableCanvas.d.ts +2 -0
  46. package/dist/src/canvas/SelectableCanvas.d.ts.map +1 -1
  47. package/dist/src/canvas/SelectableCanvas.min.mjs +1 -1
  48. package/dist/src/canvas/SelectableCanvas.min.mjs.map +1 -1
  49. package/dist/src/canvas/SelectableCanvas.mjs +33 -13
  50. package/dist/src/canvas/SelectableCanvas.mjs.map +1 -1
  51. package/dist/src/canvas/StaticCanvas.d.ts.map +1 -1
  52. package/dist/src/canvas/StaticCanvas.min.mjs +1 -1
  53. package/dist/src/canvas/StaticCanvas.min.mjs.map +1 -1
  54. package/dist/src/canvas/StaticCanvas.mjs +3 -1
  55. package/dist/src/canvas/StaticCanvas.mjs.map +1 -1
  56. package/dist/src/canvas/StaticCanvasOptions.d.ts.map +1 -1
  57. package/dist/src/canvas/StaticCanvasOptions.min.mjs.map +1 -1
  58. package/dist/src/canvas/StaticCanvasOptions.mjs.map +1 -1
  59. package/dist/src/constants.d.ts +1 -0
  60. package/dist/src/constants.d.ts.map +1 -1
  61. package/dist/src/constants.min.mjs.map +1 -1
  62. package/dist/src/constants.mjs.map +1 -1
  63. package/dist/src/controls/Control.d.ts +22 -1
  64. package/dist/src/controls/Control.d.ts.map +1 -1
  65. package/dist/src/controls/Control.min.mjs +1 -1
  66. package/dist/src/controls/Control.min.mjs.map +1 -1
  67. package/dist/src/controls/Control.mjs +45 -1
  68. package/dist/src/controls/Control.mjs.map +1 -1
  69. package/dist/src/controls/changeWidth.d.ts +22 -0
  70. package/dist/src/controls/changeWidth.d.ts.map +1 -1
  71. package/dist/src/controls/changeWidth.min.mjs +1 -1
  72. package/dist/src/controls/changeWidth.min.mjs.map +1 -1
  73. package/dist/src/controls/changeWidth.mjs +46 -18
  74. package/dist/src/controls/changeWidth.mjs.map +1 -1
  75. package/dist/src/controls/controlRendering.d.ts.map +1 -1
  76. package/dist/src/controls/controlRendering.min.mjs +1 -1
  77. package/dist/src/controls/controlRendering.min.mjs.map +1 -1
  78. package/dist/src/controls/controlRendering.mjs +18 -34
  79. package/dist/src/controls/controlRendering.mjs.map +1 -1
  80. package/dist/src/controls/index.d.ts +2 -1
  81. package/dist/src/controls/index.d.ts.map +1 -1
  82. package/dist/src/controls/index.min.mjs +1 -1
  83. package/dist/src/controls/index.mjs +1 -1
  84. package/dist/src/gradient/Gradient.d.ts.map +1 -1
  85. package/dist/src/gradient/Gradient.min.mjs +1 -1
  86. package/dist/src/gradient/Gradient.min.mjs.map +1 -1
  87. package/dist/src/gradient/Gradient.mjs +19 -6
  88. package/dist/src/gradient/Gradient.mjs.map +1 -1
  89. package/dist/src/shapes/Circle.d.ts.map +1 -1
  90. package/dist/src/shapes/Circle.min.mjs +1 -1
  91. package/dist/src/shapes/Circle.min.mjs.map +1 -1
  92. package/dist/src/shapes/Circle.mjs +10 -7
  93. package/dist/src/shapes/Circle.mjs.map +1 -1
  94. package/dist/src/shapes/Ellipse.d.ts.map +1 -1
  95. package/dist/src/shapes/Ellipse.min.mjs +1 -1
  96. package/dist/src/shapes/Ellipse.min.mjs.map +1 -1
  97. package/dist/src/shapes/Ellipse.mjs +2 -1
  98. package/dist/src/shapes/Ellipse.mjs.map +1 -1
  99. package/dist/src/shapes/Group.d.ts.map +1 -1
  100. package/dist/src/shapes/Group.min.mjs +1 -1
  101. package/dist/src/shapes/Group.min.mjs.map +1 -1
  102. package/dist/src/shapes/Group.mjs +2 -1
  103. package/dist/src/shapes/Group.mjs.map +1 -1
  104. package/dist/src/shapes/IText/IText.d.ts.map +1 -1
  105. package/dist/src/shapes/IText/IText.min.mjs.map +1 -1
  106. package/dist/src/shapes/IText/IText.mjs.map +1 -1
  107. package/dist/src/shapes/Image.d.ts +1 -1
  108. package/dist/src/shapes/Image.d.ts.map +1 -1
  109. package/dist/src/shapes/Image.min.mjs +1 -1
  110. package/dist/src/shapes/Image.min.mjs.map +1 -1
  111. package/dist/src/shapes/Image.mjs +4 -3
  112. package/dist/src/shapes/Image.mjs.map +1 -1
  113. package/dist/src/shapes/Line.d.ts.map +1 -1
  114. package/dist/src/shapes/Line.min.mjs +1 -1
  115. package/dist/src/shapes/Line.min.mjs.map +1 -1
  116. package/dist/src/shapes/Line.mjs +6 -10
  117. package/dist/src/shapes/Line.mjs.map +1 -1
  118. package/dist/src/shapes/Object/FabricObjectSVGExportMixin.d.ts.map +1 -1
  119. package/dist/src/shapes/Object/FabricObjectSVGExportMixin.min.mjs +1 -1
  120. package/dist/src/shapes/Object/FabricObjectSVGExportMixin.min.mjs.map +1 -1
  121. package/dist/src/shapes/Object/FabricObjectSVGExportMixin.mjs +5 -4
  122. package/dist/src/shapes/Object/FabricObjectSVGExportMixin.mjs.map +1 -1
  123. package/dist/src/shapes/Object/InteractiveObject.d.ts.map +1 -1
  124. package/dist/src/shapes/Object/InteractiveObject.min.mjs.map +1 -1
  125. package/dist/src/shapes/Object/InteractiveObject.mjs.map +1 -1
  126. package/dist/src/shapes/Object/Object.d.ts.map +1 -1
  127. package/dist/src/shapes/Object/Object.min.mjs +1 -1
  128. package/dist/src/shapes/Object/Object.min.mjs.map +1 -1
  129. package/dist/src/shapes/Object/Object.mjs +3 -0
  130. package/dist/src/shapes/Object/Object.mjs.map +1 -1
  131. package/dist/src/shapes/Object/ObjectGeometry.min.mjs +1 -1
  132. package/dist/src/shapes/Object/ObjectGeometry.min.mjs.map +1 -1
  133. package/dist/src/shapes/Object/ObjectGeometry.mjs +1 -1
  134. package/dist/src/shapes/Object/ObjectGeometry.mjs.map +1 -1
  135. package/dist/src/shapes/Object/types/FabricObjectProps.d.ts.map +1 -1
  136. package/dist/src/shapes/Object/types/ObjectProps.d.ts.map +1 -1
  137. package/dist/src/shapes/Path.d.ts.map +1 -1
  138. package/dist/src/shapes/Path.min.mjs.map +1 -1
  139. package/dist/src/shapes/Path.mjs +1 -2
  140. package/dist/src/shapes/Path.mjs.map +1 -1
  141. package/dist/src/shapes/Polyline.d.ts.map +1 -1
  142. package/dist/src/shapes/Polyline.min.mjs +1 -1
  143. package/dist/src/shapes/Polyline.min.mjs.map +1 -1
  144. package/dist/src/shapes/Polyline.mjs +10 -6
  145. package/dist/src/shapes/Polyline.mjs.map +1 -1
  146. package/dist/src/shapes/Rect.d.ts.map +1 -1
  147. package/dist/src/shapes/Rect.min.mjs +1 -1
  148. package/dist/src/shapes/Rect.min.mjs.map +1 -1
  149. package/dist/src/shapes/Rect.mjs +2 -1
  150. package/dist/src/shapes/Rect.mjs.map +1 -1
  151. package/dist/src/shapes/Text/StyledText.d.ts.map +1 -1
  152. package/dist/src/shapes/Text/StyledText.min.mjs.map +1 -1
  153. package/dist/src/shapes/Text/StyledText.mjs +0 -3
  154. package/dist/src/shapes/Text/StyledText.mjs.map +1 -1
  155. package/dist/src/shapes/Text/Text.d.ts.map +1 -1
  156. package/dist/src/shapes/Text/Text.min.mjs.map +1 -1
  157. package/dist/src/shapes/Text/Text.mjs.map +1 -1
  158. package/dist/src/shapes/Text/TextSVGExportMixin.d.ts.map +1 -1
  159. package/dist/src/shapes/Text/TextSVGExportMixin.min.mjs +1 -1
  160. package/dist/src/shapes/Text/TextSVGExportMixin.min.mjs.map +1 -1
  161. package/dist/src/shapes/Text/TextSVGExportMixin.mjs +5 -6
  162. package/dist/src/shapes/Text/TextSVGExportMixin.mjs.map +1 -1
  163. package/dist/src/shapes/Textbox.d.ts.map +1 -1
  164. package/dist/src/shapes/Textbox.min.mjs.map +1 -1
  165. package/dist/src/shapes/Textbox.mjs.map +1 -1
  166. package/dist/src/shapes/Triangle.d.ts.map +1 -1
  167. package/dist/src/shapes/Triangle.min.mjs.map +1 -1
  168. package/dist/src/shapes/Triangle.mjs.map +1 -1
  169. package/dist/src/util/lang_string.d.ts +1 -1
  170. package/dist/src/util/lang_string.d.ts.map +1 -1
  171. package/dist/src/util/lang_string.min.mjs +1 -1
  172. package/dist/src/util/lang_string.min.mjs.map +1 -1
  173. package/dist/src/util/lang_string.mjs +1 -1
  174. package/dist/src/util/lang_string.mjs.map +1 -1
  175. package/dist/src/util/misc/svgParsing.d.ts.map +1 -1
  176. package/dist/src/util/misc/svgParsing.min.mjs +1 -1
  177. package/dist/src/util/misc/svgParsing.min.mjs.map +1 -1
  178. package/dist/src/util/misc/svgParsing.mjs +2 -1
  179. package/dist/src/util/misc/svgParsing.mjs.map +1 -1
  180. package/dist-extensions/cropping_controls/croppingControls.mjs +140 -0
  181. package/dist-extensions/cropping_controls/croppingControls.mjs.map +1 -0
  182. package/dist-extensions/cropping_controls/croppingHandlers.mjs +228 -0
  183. package/dist-extensions/cropping_controls/croppingHandlers.mjs.map +1 -0
  184. package/dist-extensions/cropping_controls/enterCropMode.mjs +38 -0
  185. package/dist-extensions/cropping_controls/enterCropMode.mjs.map +1 -0
  186. package/dist-extensions/cropping_controls/renderCornerControl.mjs +45 -0
  187. package/dist-extensions/cropping_controls/renderCornerControl.mjs.map +1 -0
  188. package/dist-extensions/extensions/cropping_controls/croppingControls.d.ts +16 -0
  189. package/dist-extensions/extensions/cropping_controls/croppingControls.d.ts.map +1 -0
  190. package/dist-extensions/extensions/cropping_controls/croppingHandlers.d.ts +39 -0
  191. package/dist-extensions/extensions/cropping_controls/croppingHandlers.d.ts.map +1 -0
  192. package/dist-extensions/extensions/cropping_controls/enterCropMode.d.ts +7 -0
  193. package/dist-extensions/extensions/cropping_controls/enterCropMode.d.ts.map +1 -0
  194. package/dist-extensions/extensions/cropping_controls/renderCornerControl.d.ts +14 -0
  195. package/dist-extensions/extensions/cropping_controls/renderCornerControl.d.ts.map +1 -0
  196. package/dist-extensions/extensions/index.d.ts +3 -0
  197. package/dist-extensions/extensions/index.d.ts.map +1 -1
  198. package/dist-extensions/fabric-extensions.min.js +1 -1
  199. package/dist-extensions/fabric-extensions.min.js.map +1 -1
  200. package/dist-extensions/fabric.d.ts +1 -0
  201. package/dist-extensions/fabric.d.ts.map +1 -1
  202. package/dist-extensions/index.mjs +3 -0
  203. package/dist-extensions/index.mjs.map +1 -1
  204. package/dist-extensions/src/EventTypeDefs.d.ts +5 -0
  205. package/dist-extensions/src/EventTypeDefs.d.ts.map +1 -1
  206. package/dist-extensions/src/Pattern/Pattern.d.ts.map +1 -1
  207. package/dist-extensions/src/Shadow.d.ts +1 -1
  208. package/dist-extensions/src/Shadow.d.ts.map +1 -1
  209. package/dist-extensions/src/canvas/CanvasOptions.d.ts.map +1 -1
  210. package/dist-extensions/src/canvas/SelectableCanvas.d.ts +2 -0
  211. package/dist-extensions/src/canvas/SelectableCanvas.d.ts.map +1 -1
  212. package/dist-extensions/src/canvas/StaticCanvas.d.ts.map +1 -1
  213. package/dist-extensions/src/canvas/StaticCanvasOptions.d.ts.map +1 -1
  214. package/dist-extensions/src/constants.d.ts +1 -0
  215. package/dist-extensions/src/constants.d.ts.map +1 -1
  216. package/dist-extensions/src/controls/Control.d.ts +22 -1
  217. package/dist-extensions/src/controls/Control.d.ts.map +1 -1
  218. package/dist-extensions/src/controls/changeWidth.d.ts +22 -0
  219. package/dist-extensions/src/controls/changeWidth.d.ts.map +1 -1
  220. package/dist-extensions/src/controls/controlRendering.d.ts.map +1 -1
  221. package/dist-extensions/src/controls/index.d.ts +2 -1
  222. package/dist-extensions/src/controls/index.d.ts.map +1 -1
  223. package/dist-extensions/src/gradient/Gradient.d.ts.map +1 -1
  224. package/dist-extensions/src/shapes/Circle.d.ts.map +1 -1
  225. package/dist-extensions/src/shapes/Ellipse.d.ts.map +1 -1
  226. package/dist-extensions/src/shapes/Group.d.ts.map +1 -1
  227. package/dist-extensions/src/shapes/IText/IText.d.ts.map +1 -1
  228. package/dist-extensions/src/shapes/Image.d.ts.map +1 -1
  229. package/dist-extensions/src/shapes/Line.d.ts.map +1 -1
  230. package/dist-extensions/src/shapes/Object/FabricObjectSVGExportMixin.d.ts.map +1 -1
  231. package/dist-extensions/src/shapes/Object/InteractiveObject.d.ts.map +1 -1
  232. package/dist-extensions/src/shapes/Object/Object.d.ts.map +1 -1
  233. package/dist-extensions/src/shapes/Object/types/FabricObjectProps.d.ts.map +1 -1
  234. package/dist-extensions/src/shapes/Object/types/ObjectProps.d.ts.map +1 -1
  235. package/dist-extensions/src/shapes/Path.d.ts +1 -1
  236. package/dist-extensions/src/shapes/Path.d.ts.map +1 -1
  237. package/dist-extensions/src/shapes/Polyline.d.ts.map +1 -1
  238. package/dist-extensions/src/shapes/Rect.d.ts.map +1 -1
  239. package/dist-extensions/src/shapes/Text/StyledText.d.ts.map +1 -1
  240. package/dist-extensions/src/shapes/Text/Text.d.ts.map +1 -1
  241. package/dist-extensions/src/shapes/Text/TextSVGExportMixin.d.ts.map +1 -1
  242. package/dist-extensions/src/shapes/Textbox.d.ts.map +1 -1
  243. package/dist-extensions/src/shapes/Triangle.d.ts.map +1 -1
  244. package/dist-extensions/src/util/lang_string.d.ts +1 -1
  245. package/dist-extensions/src/util/lang_string.d.ts.map +1 -1
  246. package/dist-extensions/src/util/misc/svgParsing.d.ts.map +1 -1
  247. package/eslint.config.mjs +2 -0
  248. package/extensions/cropping_controls/croppingControls.spec.ts +115 -0
  249. package/extensions/cropping_controls/croppingControls.ts +150 -0
  250. package/extensions/cropping_controls/croppingHandlers.spec.ts +579 -0
  251. package/extensions/cropping_controls/croppingHandlers.ts +285 -0
  252. package/extensions/cropping_controls/enterCropMode.ts +30 -0
  253. package/extensions/cropping_controls/renderCornerControl.ts +53 -0
  254. package/extensions/index.ts +9 -0
  255. package/fabric.ts +1 -0
  256. package/package.json +17 -8
  257. package/src/ClassRegistry.spec.ts +18 -19
  258. package/src/EventTypeDefs.ts +15 -11
  259. package/src/Pattern/Pattern.spec.ts +12 -0
  260. package/src/Pattern/Pattern.ts +3 -2
  261. package/src/Shadow.ts +9 -8
  262. package/src/brushes/PencilBrush.spec.ts +11 -11
  263. package/src/canvas/Canvas-dispose.spec.ts +8 -7
  264. package/src/canvas/Canvas.spec.ts +27 -29
  265. package/src/canvas/CanvasOptions.ts +2 -1
  266. package/src/canvas/SelectableCanvas.ts +38 -15
  267. package/src/canvas/StaticCanvas.spec.ts +20 -0
  268. package/src/canvas/StaticCanvas.ts +7 -4
  269. package/src/canvas/StaticCanvasOptions.ts +1 -3
  270. package/src/constants.ts +1 -0
  271. package/src/controls/Control.spec.ts +102 -0
  272. package/src/controls/Control.ts +71 -2
  273. package/src/controls/changeHeight.spec.ts +147 -0
  274. package/src/controls/changeWidth.ts +68 -35
  275. package/src/controls/controlRendering.ts +20 -48
  276. package/src/controls/index.ts +7 -1
  277. package/src/gradient/Gradient.spec.ts +101 -46
  278. package/src/gradient/Gradient.ts +27 -14
  279. package/src/shapes/Circle.spec.ts +10 -39
  280. package/src/shapes/Circle.ts +11 -11
  281. package/src/shapes/Ellipse.spec.ts +8 -37
  282. package/src/shapes/Ellipse.ts +7 -7
  283. package/src/shapes/Group.ts +3 -3
  284. package/src/shapes/IText/IText-click-behavior.spec.ts +36 -49
  285. package/src/shapes/IText/IText.ts +5 -6
  286. package/src/shapes/IText/ITextKeyBehavior.test.ts +0 -1
  287. package/src/shapes/IText/__snapshots__/ITextBehavior.test.ts.snap +6 -6
  288. package/src/shapes/Image.spec.ts +17 -33
  289. package/src/shapes/Image.ts +15 -11
  290. package/src/shapes/Line.spec.ts +4 -30
  291. package/src/shapes/Line.ts +11 -16
  292. package/src/shapes/Object/FabricObjectSVGExportMixin.ts +11 -4
  293. package/src/shapes/Object/InteractiveObject.ts +4 -4
  294. package/src/shapes/Object/Object.ts +6 -5
  295. package/src/shapes/Object/ObjectGeometry.spec.ts +15 -0
  296. package/src/shapes/Object/ObjectGeometry.ts +1 -1
  297. package/src/shapes/Object/objectSvgExport.spec.ts +112 -0
  298. package/src/shapes/Object/types/FabricObjectProps.ts +1 -4
  299. package/src/shapes/Object/types/ObjectProps.ts +1 -3
  300. package/src/shapes/Path.spec.ts +4 -27
  301. package/src/shapes/Path.ts +2 -4
  302. package/src/shapes/Polygon.spec.ts +4 -31
  303. package/src/shapes/Polyline.spec.ts +4 -31
  304. package/src/shapes/Polyline.ts +11 -12
  305. package/src/shapes/Rect.spec.ts +25 -33
  306. package/src/shapes/Rect.ts +7 -7
  307. package/src/shapes/Text/StyledText.ts +0 -3
  308. package/src/shapes/Text/Text.spec.ts +3 -32
  309. package/src/shapes/Text/Text.ts +5 -6
  310. package/src/shapes/Text/TextSVGExportMixin.spec.ts +9 -0
  311. package/src/shapes/Text/TextSVGExportMixin.ts +14 -16
  312. package/src/shapes/Text/__snapshots__/Text.spec.ts.snap +1 -1
  313. package/src/shapes/Text/__snapshots__/TextSVGExportMixin.spec.ts.snap +1 -1
  314. package/src/shapes/Textbox.spec.ts +5 -5
  315. package/src/shapes/Textbox.ts +6 -5
  316. package/src/shapes/Triangle.ts +4 -4
  317. package/src/shapes/__snapshots__/Image.spec.ts.snap +4 -4
  318. package/src/shapes/__snapshots__/Textbox.spec.ts.snap +5 -5
  319. package/src/util/lang_string.ts +3 -2
  320. package/src/util/misc/svgParsing.ts +2 -1
  321. package/tsconfig.spec.json +1 -0
  322. package/vitest.config.ts +12 -2
  323. package/vitest.extend.ts +6 -2
@@ -0,0 +1,285 @@
1
+ import type {
2
+ TModificationEvents,
3
+ Transform,
4
+ TransformActionHandler,
5
+ FabricImage,
6
+ ObjectEvents,
7
+ Control,
8
+ TMat2D,
9
+ } from 'fabric';
10
+ import { controlsUtils, Point, util } from 'fabric';
11
+
12
+ const { wrapWithFixedAnchor, wrapWithFireEvent } = controlsUtils;
13
+
14
+ /**
15
+ * Wrap controlsUtils.changeObjectWidth with image constrains
16
+ */
17
+ export const changeImageWidth: TransformActionHandler = (
18
+ eventData,
19
+ transform,
20
+ x,
21
+ y,
22
+ ) => {
23
+ const { target } = transform;
24
+ const { width } = target;
25
+ const image = target as FabricImage;
26
+ const modified = controlsUtils.changeObjectWidth(eventData, transform, x, y);
27
+ const availableWidth = image._element.width - image.cropX;
28
+ if (modified) {
29
+ if (image.width > availableWidth) {
30
+ image.width = availableWidth;
31
+ }
32
+ if (image.width < 1) {
33
+ image.width = 1;
34
+ }
35
+ }
36
+ return width !== image.width;
37
+ };
38
+
39
+ export const changeCropWidth = wrapWithFireEvent(
40
+ 'CROPPING' as TModificationEvents,
41
+ wrapWithFixedAnchor(changeImageWidth),
42
+ );
43
+
44
+ /**
45
+ * Wrap controlsUtils.changeObjectHeight with image constrains
46
+ */
47
+ export const changeImageHeight: TransformActionHandler = (
48
+ eventData,
49
+ transform,
50
+ x,
51
+ y,
52
+ ) => {
53
+ const { target } = transform;
54
+ const { height } = target;
55
+ const image = target as FabricImage;
56
+ const modified = controlsUtils.changeObjectHeight(eventData, transform, x, y);
57
+ const availableHeight = image._element.height - image.cropY;
58
+ if (modified) {
59
+ if (image.height > availableHeight) {
60
+ image.height = availableHeight;
61
+ }
62
+ if (image.height < 1) {
63
+ image.height = 1;
64
+ }
65
+ }
66
+ return height !== image.height;
67
+ };
68
+
69
+ export const changeCropHeight = wrapWithFireEvent(
70
+ 'CROPPING' as TModificationEvents,
71
+ wrapWithFixedAnchor(changeImageHeight),
72
+ );
73
+
74
+ export const changeImageCropX: TransformActionHandler = (
75
+ eventData,
76
+ transform,
77
+ x,
78
+ y,
79
+ ) => {
80
+ const { target } = transform;
81
+ const image = target as FabricImage;
82
+ const { width, cropX } = image;
83
+ const modified = controlsUtils.changeObjectWidth(eventData, transform, x, y);
84
+ let newCropX = cropX + width - image.width;
85
+ image.width = width;
86
+ if (modified) {
87
+ if (newCropX < 0) {
88
+ newCropX = 0;
89
+ }
90
+ image.cropX = newCropX;
91
+ // calculate new width on the base of how much crop we have now
92
+ image.width += cropX - newCropX;
93
+ }
94
+ return newCropX !== cropX;
95
+ };
96
+
97
+ export const changeImageCropY: TransformActionHandler = (
98
+ eventData,
99
+ transform,
100
+ x,
101
+ y,
102
+ ) => {
103
+ const { target } = transform;
104
+ const image = target as FabricImage;
105
+ const { height, cropY } = image;
106
+ const modified = controlsUtils.changeObjectHeight(eventData, transform, x, y);
107
+ let newCropY = cropY + height - image.height;
108
+ image.height = height;
109
+ if (modified) {
110
+ if (newCropY < 0) {
111
+ newCropY = 0;
112
+ }
113
+ image.cropY = newCropY;
114
+ image.height += cropY - newCropY;
115
+ }
116
+ return newCropY !== cropY;
117
+ };
118
+
119
+ export const changeCropX = wrapWithFireEvent(
120
+ 'CROPPING' as TModificationEvents,
121
+ wrapWithFixedAnchor(changeImageCropX),
122
+ );
123
+
124
+ export const changeCropY = wrapWithFireEvent(
125
+ 'CROPPING' as TModificationEvents,
126
+ wrapWithFixedAnchor(changeImageCropY),
127
+ );
128
+
129
+ /**
130
+ * A function to counter the move action and change cropX/cropY of an image
131
+ * Keep the image steady, but moves it inside its own cropping rectangle
132
+ */
133
+ export const cropPanMoveHandler = ({ transform }: ObjectEvents['moving']) => {
134
+ // this makes the image pan too fast.
135
+ const { target, original } = transform as Transform;
136
+ const fabricImage = target as FabricImage;
137
+ const p = new Point(
138
+ target.left - original.left,
139
+ target.top - original.top,
140
+ ).transform(
141
+ util.invertTransform(
142
+ util.createRotateMatrix({ angle: fabricImage.getTotalAngle() }),
143
+ ),
144
+ );
145
+ let cropX = original.cropX! - p.x / fabricImage.scaleX;
146
+ let cropY = original.cropY! - p.y / fabricImage.scaleY;
147
+ const { width, height, _element } = fabricImage;
148
+ if (cropX < 0) {
149
+ cropX = 0;
150
+ }
151
+ if (cropY < 0) {
152
+ cropY = 0;
153
+ }
154
+ if (cropX + width > _element.width) {
155
+ cropX = _element.width - width;
156
+ }
157
+ if (cropY + height > _element.height) {
158
+ cropY = _element.height - height;
159
+ }
160
+ fabricImage.cropX = cropX;
161
+ fabricImage.cropY = cropY;
162
+ fabricImage.left = original.left;
163
+ fabricImage.top = original.top;
164
+ };
165
+
166
+ /**
167
+ * This position handler works only for this specific use case.
168
+ * It does not support padding nor offset, and it reduces all possible positions
169
+ * to the main 4 corners only.
170
+ * Any position that is < 0 is the extreme left/top, the rest are right/bottom
171
+ */
172
+ export function ghostScalePositionHandler(
173
+ this: Control,
174
+ dim: Point, // currentDimension
175
+ finalMatrix: TMat2D,
176
+ fabricObject: FabricImage,
177
+ // currentControl: Control,
178
+ ) {
179
+ const matrix = fabricObject.calcTransformMatrix();
180
+ const vpt = fabricObject.getViewportTransform();
181
+ const _finalMatrix = util.multiplyTransformMatrices(vpt, matrix);
182
+
183
+ let x = 0;
184
+ let y = 0;
185
+ if (this.x < 0) {
186
+ x = -fabricObject.width / 2 - fabricObject.cropX;
187
+ } else {
188
+ x =
189
+ fabricObject.getElement().width -
190
+ fabricObject.width / 2 -
191
+ fabricObject.cropX;
192
+ }
193
+
194
+ if (this.y < 0) {
195
+ y = -fabricObject.height / 2 - fabricObject.cropY;
196
+ } else {
197
+ y =
198
+ fabricObject.getElement().height -
199
+ fabricObject.height / 2 -
200
+ fabricObject.cropY;
201
+ }
202
+ return new Point(x, y).transform(_finalMatrix);
203
+ }
204
+
205
+ const calcScale = (currentPoint: Point, height: number, width: number) =>
206
+ Math.min(Math.abs(currentPoint.x / width), Math.abs(currentPoint.y / height));
207
+
208
+ /**
209
+ * Action handler generator that handles scaling of an image in crop mode.
210
+ * The goal is to keep the current bounding box steady.
211
+ * So this action handler has its own calculations for a dynamic anchor point
212
+ */
213
+ export const scaleEquallyCropGenerator =
214
+ (cx: number, cy: number): TransformActionHandler =>
215
+ (eventData, transform, x, y) => {
216
+ const { target } = transform as unknown as { target: FabricImage };
217
+ const { width: fullWidth, height: fullHeight } = target.getElement();
218
+ const remainderX = fullWidth - target.width - target.cropX;
219
+ const remainderY = fullHeight - target.height - target.cropY;
220
+ const anchorOriginX =
221
+ cx < 0 ? 1 + remainderX / target.width : -target.cropX / target.width;
222
+ const anchorOriginY =
223
+ cy < 0 ? 1 + remainderY / target.height : -target.cropY / target.height;
224
+ const constraint = target.translateToOriginPoint(
225
+ target.getCenterPoint(),
226
+ anchorOriginX,
227
+ anchorOriginY,
228
+ );
229
+ const newPoint = controlsUtils.getLocalPoint(
230
+ transform,
231
+ anchorOriginX,
232
+ anchorOriginY,
233
+ x,
234
+ y,
235
+ );
236
+ const scale = calcScale(newPoint, fullHeight, fullWidth);
237
+ const scaleChangeX = scale / target.scaleX;
238
+ const scaleChangeY = scale / target.scaleY;
239
+ const scaledRemainderX = remainderX / scaleChangeX;
240
+ const scaledRemainderY = remainderY / scaleChangeY;
241
+ const newWidth = target.width / scaleChangeX;
242
+ const newHeight = target.height / scaleChangeY;
243
+ const newCropX =
244
+ cx < 0
245
+ ? fullWidth - newWidth - scaledRemainderX
246
+ : target.cropX / scaleChangeX;
247
+ const newCropY =
248
+ cy < 0
249
+ ? fullHeight - newHeight - scaledRemainderY
250
+ : target.cropY / scaleChangeY;
251
+
252
+ if (
253
+ (cx < 0 ? scaledRemainderX : newCropX) + newWidth > fullWidth ||
254
+ (cy < 0 ? scaledRemainderY : newCropY) + newHeight > fullHeight
255
+ ) {
256
+ return false;
257
+ }
258
+
259
+ target.scaleX = scale;
260
+ target.scaleY = scale;
261
+ target.width = newWidth;
262
+ target.height = newHeight;
263
+ target.cropX = newCropX;
264
+ target.cropY = newCropY;
265
+ const newAnchorOriginX =
266
+ cx < 0 ? 1 + scaledRemainderX / newWidth : -newCropX / newWidth;
267
+ const newAnchorOriginY =
268
+ cy < 0 ? 1 + scaledRemainderY / newHeight : -newCropY / newHeight;
269
+ target.setPositionByOrigin(constraint, newAnchorOriginX, newAnchorOriginY);
270
+ return true;
271
+ };
272
+
273
+ export function renderGhostImage(
274
+ this: FabricImage,
275
+ { ctx }: { ctx: CanvasRenderingContext2D },
276
+ ) {
277
+ const alpha = ctx.globalAlpha;
278
+ ctx.globalAlpha *= 0.5;
279
+ ctx.drawImage(
280
+ this._element,
281
+ -this.width / 2 - this.cropX,
282
+ -this.height / 2 - this.cropY,
283
+ );
284
+ ctx.globalAlpha = alpha;
285
+ }
@@ -0,0 +1,30 @@
1
+ import { type FabricImage, type TPointerEventInfo } from 'fabric';
2
+ import { createImageCroppingControls } from './croppingControls';
3
+ import { cropPanMoveHandler, renderGhostImage } from './croppingHandlers';
4
+ /**
5
+ * Coordinates the change to image to enter crop mode and returns
6
+ * a function to exit crop mode
7
+ */
8
+ export const enterCropMode = function enterCropMode(
9
+ this: (args: TPointerEventInfo) => void,
10
+ { target }: TPointerEventInfo,
11
+ ) {
12
+ const fabricImage = target as FabricImage;
13
+ const { controls, padding } = fabricImage;
14
+ fabricImage.padding = 0;
15
+ fabricImage.controls = createImageCroppingControls();
16
+ fabricImage.on('moving', cropPanMoveHandler);
17
+ fabricImage.on('before:render', renderGhostImage);
18
+ fabricImage.setCoords();
19
+ const exitCropMode = () => {
20
+ fabricImage.padding = padding;
21
+ fabricImage.off('moving', cropPanMoveHandler);
22
+ fabricImage.off('before:render', renderGhostImage);
23
+ fabricImage.controls = controls;
24
+ fabricImage.setCoords();
25
+ fabricImage.once('mousedblclick', enterCropMode);
26
+ fabricImage.canvas?.requestRenderAll();
27
+ };
28
+ fabricImage.once('mousedblclick', exitCropMode);
29
+ fabricImage.canvas?.requestRenderAll();
30
+ };
@@ -0,0 +1,53 @@
1
+ import {
2
+ type ControlRenderingStyleOverride,
3
+ type InteractiveFabricObject,
4
+ util,
5
+ type Control,
6
+ } from 'fabric';
7
+
8
+ const { degreesToRadians } = util;
9
+
10
+ /**
11
+ * Render a control for the main corners of a cropping image
12
+ * This function is written to respect object properties like transparentCorners, cornerSize
13
+ * cornerColor, cornerStrokeColor
14
+ * plus the addition of offsetY and offsetX.
15
+ * @param {CanvasRenderingContext2D} ctx context to render on
16
+ * @param {Number} left x coordinate where the control center should be
17
+ * @param {Number} top y coordinate where the control center should be
18
+ * @param {Object} styleOverride override for FabricObject controls style
19
+ * @param {FabricObject} fabricObject the fabric object for which we are rendering controls
20
+ */
21
+ export function renderCornerControl(
22
+ this: Control,
23
+ ctx: CanvasRenderingContext2D,
24
+ left: number,
25
+ top: number,
26
+ styleOverride: ControlRenderingStyleOverride,
27
+ fabricObject: InteractiveFabricObject,
28
+ ) {
29
+ ctx.save();
30
+ const { stroke, xSize, ySize, opName } = this.commonRenderProps(
31
+ ctx,
32
+ left,
33
+ top,
34
+ fabricObject,
35
+ styleOverride,
36
+ ),
37
+ xSizeBy2 = xSize / 2,
38
+ ySizeBy2 = ySize / 2;
39
+ // angle is relative to canvas plane
40
+ ctx.rotate(degreesToRadians(this.angle));
41
+ ctx.beginPath();
42
+ ctx.moveTo(-ySizeBy2, 0);
43
+ ctx.lineTo(-ySizeBy2, xSizeBy2);
44
+ ctx.lineTo(ySizeBy2, xSizeBy2);
45
+ ctx.lineTo(ySizeBy2, ySizeBy2);
46
+ ctx.lineTo(xSizeBy2, ySizeBy2);
47
+ ctx.lineTo(xSizeBy2, -ySizeBy2);
48
+ ctx.lineTo(-ySizeBy2, -ySizeBy2);
49
+ ctx.closePath();
50
+ ctx[opName]();
51
+ stroke && ctx.stroke();
52
+ ctx.restore();
53
+ }
@@ -16,3 +16,12 @@ export {
16
16
  pinchEventHandler,
17
17
  rotateEventHandler,
18
18
  } from './westures_integration';
19
+
20
+ export { createImageCroppingControls } from './cropping_controls/croppingControls';
21
+ export {
22
+ changeCropY,
23
+ changeCropX,
24
+ changeCropWidth,
25
+ changeCropHeight,
26
+ } from './cropping_controls/croppingHandlers';
27
+ export { enterCropMode } from './cropping_controls/enterCropMode';
package/fabric.ts CHANGED
@@ -178,5 +178,6 @@ export { parseSVGDocument } from './src/parser/parseSVGDocument';
178
178
 
179
179
  export { Control } from './src/controls/Control';
180
180
  export * as controlsUtils from './src/controls';
181
+ export type { ControlRenderingStyleOverride } from './src/controls';
181
182
 
182
183
  export * from './src/filters';
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "fabric",
3
3
  "description": "Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.",
4
4
  "homepage": "http://fabricjs.com/",
5
- "version": "7.0.0",
5
+ "version": "7.2.0",
6
6
  "author": "Juriy Zaytsev <kangax@gmail.com>",
7
7
  "contributors": [
8
8
  {
@@ -63,7 +63,8 @@
63
63
  "local-server": "serve ./ -l tcp://localhost:8080",
64
64
  "lint": "eslint src extensions",
65
65
  "prettier:check": "prettier --check .",
66
- "prettier:write": "prettier --write ."
66
+ "prettier:write": "prettier --write .",
67
+ "prepare": "husky install"
67
68
  },
68
69
  "devDependencies": {
69
70
  "@babel/cli": "^7.28.3",
@@ -71,7 +72,7 @@
71
72
  "@babel/preset-env": "^7.28.3",
72
73
  "@babel/preset-typescript": "^7.27.1",
73
74
  "@eslint/js": "^9.37.0",
74
- "@playwright/test": "^1.56.0",
75
+ "@playwright/test": "^1.58.1",
75
76
  "@rollup/plugin-babel": "^6.0.4",
76
77
  "@rollup/plugin-json": "^6.1.0",
77
78
  "@rollup/plugin-terser": "^0.4.4",
@@ -79,26 +80,29 @@
79
80
  "@types/jsdom": "^21.1.7",
80
81
  "@types/micromatch": "^4.0.9",
81
82
  "@types/node": "^24.7.0",
82
- "@vitest/browser-playwright": "^4.0.2",
83
- "@vitest/coverage-v8": "^4.0.2",
84
- "@vitest/ui": "^4.0.2",
83
+ "@vitest/browser-playwright": "^4.0.18",
84
+ "@vitest/coverage-v8": "^4.0.18",
85
+ "@vitest/ui": "^4.0.18",
85
86
  "babel-plugin-transform-imports": "git+https://git@github.com/fabricjs/babel-plugin-transform-imports.git",
86
87
  "commander": "^14.0.1",
87
88
  "es-toolkit": "1.40.0",
88
89
  "eslint-config-prettier": "^10.1.8",
90
+ "husky": "^9.1.7",
89
91
  "inquirer": "^12.9.6",
92
+ "lint-staged": "^16.2.7",
90
93
  "micromatch": "^4.0.8",
91
94
  "nyc": "^17.1.0",
92
- "prettier": "^3.6.2",
95
+ "prettier": "^3.8.1",
93
96
  "ps-list": "^9.0.0",
94
97
  "rollup": "^4.52.4",
95
98
  "semver": "^7.7.3",
96
99
  "serve": "^14.2.5",
100
+ "tsc-files": "^1.1.4",
97
101
  "tslib": "^2.8.1",
98
102
  "typescript": "^5.9.3",
99
103
  "typescript-eslint": "^8.46.0",
100
104
  "v8-to-istanbul": "^9.3.0",
101
- "vitest": "^4.0.2",
105
+ "vitest": "^4.0.18",
102
106
  "westures": "^1.1.1"
103
107
  },
104
108
  "engines": {
@@ -153,5 +157,10 @@
153
157
  "optionalDependencies": {
154
158
  "canvas": "^3.2.0",
155
159
  "jsdom": "^26.1.0"
160
+ },
161
+ "lint-staged": {
162
+ "*.{js,md,css,ts,tsx,jsx,json}": "eslint --fix",
163
+ "*.{js,css,md,ts,tsx,jsx,json}": "prettier --write",
164
+ "**/*.ts !(**/*.spec.ts) !(**/*.test.ts) !(vitest*.ts)": "tsc-files --noEmit"
156
165
  }
157
166
  }
@@ -1,17 +1,16 @@
1
- import {
2
- ClassRegistry,
3
- classRegistry as genericClassRegistryInstance,
4
- JSON,
5
- } from './ClassRegistry';
6
- import './shapes/Object/FabricObject';
1
+ import type { ClassRegistry } from './ClassRegistry';
7
2
 
8
- import { describe, expect, beforeEach, it } from 'vitest';
3
+ import { describe, expect, beforeEach, afterEach, it, vi } from 'vitest';
9
4
 
10
5
  describe('ClassRegistry', () => {
11
6
  let classRegistry: ClassRegistry;
12
- beforeEach(() => {
7
+ beforeEach(async () => {
8
+ const { ClassRegistry } = await import('./ClassRegistry');
13
9
  classRegistry = new ClassRegistry();
14
10
  });
11
+ afterEach(() => {
12
+ vi.resetModules();
13
+ });
15
14
  it('will error if a class is request that is not registered', () => {
16
15
  expect(() => classRegistry.getClass('any')).toThrow(
17
16
  'No class registered for any',
@@ -57,30 +56,30 @@ describe('ClassRegistry', () => {
57
56
  expect(resolved, 'resolved different classes').not.toBe(resolvedSvg);
58
57
  });
59
58
  it('legacy resolution preparation', async () => {
60
- genericClassRegistryInstance[JSON].delete('rect');
61
- genericClassRegistryInstance[JSON].delete('i-text');
62
- genericClassRegistryInstance[JSON].delete('activeSelelection');
63
- genericClassRegistryInstance[JSON].delete('object');
64
- expect(genericClassRegistryInstance.has('rect')).toBe(false);
65
- expect(genericClassRegistryInstance.has('i-text')).toBe(false);
66
- expect(genericClassRegistryInstance.has('activeSelection')).toBe(false);
67
- expect(genericClassRegistryInstance.has('object')).toBe(false);
59
+ const { classRegistry: freshRegistry } = await import('./ClassRegistry');
60
+
61
+ // Registry should be empty before any shape classes are imported
62
+ expect(freshRegistry.has('rect')).toBe(false);
63
+ expect(freshRegistry.has('i-text')).toBe(false);
64
+ expect(freshRegistry.has('activeSelection')).toBe(false);
65
+ expect(freshRegistry.has('object')).toBe(false);
68
66
  });
69
67
  it('legacy resolution', async () => {
68
+ const { classRegistry: freshRegistry } = await import('./ClassRegistry');
70
69
  const { Rect } = await import('./shapes/Rect');
71
70
  const { IText } = await import('./shapes/IText/IText');
72
71
  const { ActiveSelection } = await import('./shapes/ActiveSelection');
73
72
  // const { FabricObject } = await import('./shapes/Object/FabricObject');
74
73
  expect(
75
- genericClassRegistryInstance.getClass('rect'),
74
+ freshRegistry.getClass('rect'),
76
75
  'resolves Rect class correctly',
77
76
  ).toBe(Rect);
78
77
  expect(
79
- genericClassRegistryInstance.getClass('i-text'),
78
+ freshRegistry.getClass('i-text'),
80
79
  'resolves IText class correctly',
81
80
  ).toBe(IText);
82
81
  expect(
83
- genericClassRegistryInstance.getClass('activeSelection'),
82
+ freshRegistry.getClass('activeSelection'),
84
83
  'resolves ActiveSelection class correctly',
85
84
  ).toBe(ActiveSelection);
86
85
  // expect(
@@ -94,6 +94,8 @@ export type Transform = {
94
94
  original: ReturnType<typeof saveObjectTransform> & {
95
95
  originX: TOriginX;
96
96
  originY: TOriginY;
97
+ cropX?: number;
98
+ cropY?: number;
97
99
  };
98
100
  actionPerformed: boolean;
99
101
  };
@@ -106,8 +108,9 @@ interface TEventWithTarget<E extends Event = TPointerEvent> extends TEvent<E> {
106
108
  target: FabricObject;
107
109
  }
108
110
 
109
- export interface BasicTransformEvent<E extends Event = TPointerEvent>
110
- extends TEvent<E> {
111
+ export interface BasicTransformEvent<
112
+ E extends Event = TPointerEvent,
113
+ > extends TEvent<E> {
111
114
  transform: Transform;
112
115
  /* This pointer is usually a scenePoint. It isn't in the case of actions inside groups,
113
116
  * where it becomes a point relative to the group center
@@ -161,8 +164,9 @@ type CanvasModificationEvents = {
161
164
  'object:modified': ModifiedEvent;
162
165
  };
163
166
 
164
- export interface TPointerEventInfo<E extends TPointerEvent = TPointerEvent>
165
- extends TEvent<E> {
167
+ export interface TPointerEventInfo<
168
+ E extends TPointerEvent = TPointerEvent,
169
+ > extends TEvent<E> {
166
170
  target?: FabricObject;
167
171
  subTargets?: FabricObject[];
168
172
  transform?: Transform | null;
@@ -170,8 +174,9 @@ export interface TPointerEventInfo<E extends TPointerEvent = TPointerEvent>
170
174
  viewportPoint: Point;
171
175
  }
172
176
 
173
- interface SimpleEventHandler<T extends Event = TPointerEvent>
174
- extends TEvent<T> {
177
+ interface SimpleEventHandler<
178
+ T extends Event = TPointerEvent,
179
+ > extends TEvent<T> {
175
180
  target?: FabricObject;
176
181
  subTargets: FabricObject[];
177
182
  }
@@ -286,10 +291,7 @@ export interface MiscEvents {
286
291
  }
287
292
 
288
293
  export interface ObjectEvents
289
- extends ObjectPointerEvents,
290
- DnDEvents,
291
- MiscEvents,
292
- ObjectModificationEvents {
294
+ extends ObjectPointerEvents, DnDEvents, MiscEvents, ObjectModificationEvents {
293
295
  // selection
294
296
  selected: Partial<TEvent> & {
295
297
  target: FabricObject;
@@ -303,6 +305,7 @@ export interface ObjectEvents
303
305
 
304
306
  // erasing
305
307
  'erasing:end': { path: FabricObject };
308
+ 'before:render': { ctx: CanvasRenderingContext2D };
306
309
  }
307
310
 
308
311
  export interface StaticCanvasEvents extends CollectionEvents {
@@ -317,7 +320,8 @@ export interface StaticCanvasEvents extends CollectionEvents {
317
320
  }
318
321
 
319
322
  export interface CanvasEvents
320
- extends StaticCanvasEvents,
323
+ extends
324
+ StaticCanvasEvents,
321
325
  CanvasPointerEvents,
322
326
  CanvasDnDEvents,
323
327
  MiscEvents,
@@ -187,4 +187,16 @@ describe('Pattern', () => {
187
187
  const obj = await Rect.fromObject(rectObj);
188
188
  expect(obj.fill instanceof Pattern).toBeTruthy();
189
189
  });
190
+
191
+ describe('attribute injection', () => {
192
+ it('escapes correctly the src', () => {
193
+ const pattern = new Pattern({
194
+ source: { src: '"><svg onload=alert(1)>', width: 10, height: 10 },
195
+ });
196
+ const svg = pattern.toSVG({ width: 100, height: 100 });
197
+ expect(svg).toContain(
198
+ 'xlink:href="&quot;&gt;&lt;svg onload=alert(1)&gt;"',
199
+ );
200
+ });
201
+ });
190
202
  });
@@ -12,6 +12,7 @@ import type {
12
12
  SerializedPatternOptions,
13
13
  } from './types';
14
14
  import { log } from '../util/internals/console';
15
+ import { escapeXml } from '../util/lang_string';
15
16
 
16
17
  /**
17
18
  * @see {@link http://fabric5.fabricjs.com/patterns demo}
@@ -177,12 +178,12 @@ export class Pattern {
177
178
  : ifNaN((patternSource as HTMLImageElement).height / height, 0);
178
179
 
179
180
  return [
180
- `<pattern id="SVGID_${id}" x="${patternOffsetX}" y="${patternOffsetY}" width="${patternWidth}" height="${patternHeight}">`,
181
+ `<pattern id="SVGID_${escapeXml(id)}" x="${patternOffsetX}" y="${patternOffsetY}" width="${patternWidth}" height="${patternHeight}">`,
181
182
  `<image x="0" y="0" width="${
182
183
  (patternSource as HTMLImageElement).width
183
184
  }" height="${
184
185
  (patternSource as HTMLImageElement).height
185
- }" xlink:href="${this.sourceToString()}"></image>`,
186
+ }" xlink:href="${escapeXml(this.sourceToString())}"></image>`,
186
187
  `</pattern>`,
187
188
  '',
188
189
  ].join('\n');