@tiptap/core 3.9.1 → 3.10.1

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.
package/dist/index.cjs CHANGED
@@ -32,6 +32,8 @@ __export(index_exports, {
32
32
  NodePos: () => NodePos,
33
33
  NodeView: () => NodeView,
34
34
  PasteRule: () => PasteRule,
35
+ ResizableNodeView: () => ResizableNodeView,
36
+ ResizableNodeview: () => ResizableNodeview,
35
37
  Tracker: () => Tracker,
36
38
  callOrReturn: () => callOrReturn,
37
39
  canInsertNode: () => canInsertNode,
@@ -3647,9 +3649,13 @@ var ExtensionManager = class {
3647
3649
  if (!addNodeView) {
3648
3650
  return [];
3649
3651
  }
3652
+ const nodeViewResult = addNodeView();
3653
+ if (!nodeViewResult) {
3654
+ return [];
3655
+ }
3650
3656
  const nodeview = (node, view, getPos, decorations, innerDecorations) => {
3651
3657
  const HTMLAttributes = getRenderedAttributes(node, extensionAttributes);
3652
- return addNodeView()({
3658
+ return nodeViewResult({
3653
3659
  // pass-through
3654
3660
  node,
3655
3661
  view,
@@ -5136,6 +5142,540 @@ var h = (tag, attributes) => {
5136
5142
  return [tag, rest, children];
5137
5143
  };
5138
5144
 
5145
+ // src/lib/ResizableNodeView.ts
5146
+ var isTouchEvent = (e) => {
5147
+ return "touches" in e;
5148
+ };
5149
+ var ResizableNodeView = class {
5150
+ /**
5151
+ * Creates a new ResizableNodeView instance.
5152
+ *
5153
+ * The constructor sets up the resize handles, applies initial sizing from
5154
+ * node attributes, and configures all resize behavior options.
5155
+ *
5156
+ * @param options - Configuration options for the resizable node view
5157
+ */
5158
+ constructor(options) {
5159
+ /** Active resize handle directions */
5160
+ this.directions = ["bottom-left", "bottom-right", "top-left", "top-right"];
5161
+ /** Minimum allowed dimensions */
5162
+ this.minSize = {
5163
+ height: 8,
5164
+ width: 8
5165
+ };
5166
+ /** Whether to always preserve aspect ratio */
5167
+ this.preserveAspectRatio = false;
5168
+ /** CSS class names for elements */
5169
+ this.classNames = {
5170
+ container: "",
5171
+ wrapper: "",
5172
+ handle: "",
5173
+ resizing: ""
5174
+ };
5175
+ /** Initial width of the element (for aspect ratio calculation) */
5176
+ this.initialWidth = 0;
5177
+ /** Initial height of the element (for aspect ratio calculation) */
5178
+ this.initialHeight = 0;
5179
+ /** Calculated aspect ratio (width / height) */
5180
+ this.aspectRatio = 1;
5181
+ /** Whether a resize operation is currently active */
5182
+ this.isResizing = false;
5183
+ /** The handle currently being dragged */
5184
+ this.activeHandle = null;
5185
+ /** Starting mouse X position when resize began */
5186
+ this.startX = 0;
5187
+ /** Starting mouse Y position when resize began */
5188
+ this.startY = 0;
5189
+ /** Element width when resize began */
5190
+ this.startWidth = 0;
5191
+ /** Element height when resize began */
5192
+ this.startHeight = 0;
5193
+ /** Whether Shift key is currently pressed (for temporary aspect ratio lock) */
5194
+ this.isShiftKeyPressed = false;
5195
+ /**
5196
+ * Handles mouse movement during an active resize.
5197
+ *
5198
+ * Calculates the delta from the starting position, computes new dimensions
5199
+ * based on the active handle direction, applies constraints and aspect ratio,
5200
+ * then updates the element's style and calls the onResize callback.
5201
+ *
5202
+ * @param event - The mouse move event
5203
+ */
5204
+ this.handleMouseMove = (event) => {
5205
+ if (!this.isResizing || !this.activeHandle) {
5206
+ return;
5207
+ }
5208
+ const deltaX = event.clientX - this.startX;
5209
+ const deltaY = event.clientY - this.startY;
5210
+ this.handleResize(deltaX, deltaY);
5211
+ };
5212
+ this.handleTouchMove = (event) => {
5213
+ if (!this.isResizing || !this.activeHandle) {
5214
+ return;
5215
+ }
5216
+ const touch = event.touches[0];
5217
+ if (!touch) {
5218
+ return;
5219
+ }
5220
+ const deltaX = touch.clientX - this.startX;
5221
+ const deltaY = touch.clientY - this.startY;
5222
+ this.handleResize(deltaX, deltaY);
5223
+ };
5224
+ /**
5225
+ * Completes the resize operation when the mouse button is released.
5226
+ *
5227
+ * Captures final dimensions, calls the onCommit callback to persist changes,
5228
+ * removes the resizing state and class, and cleans up document-level listeners.
5229
+ */
5230
+ this.handleMouseUp = () => {
5231
+ if (!this.isResizing) {
5232
+ return;
5233
+ }
5234
+ const finalWidth = this.element.offsetWidth;
5235
+ const finalHeight = this.element.offsetHeight;
5236
+ this.onCommit(finalWidth, finalHeight);
5237
+ this.isResizing = false;
5238
+ this.activeHandle = null;
5239
+ this.container.dataset.resizeState = "false";
5240
+ if (this.classNames.resizing) {
5241
+ this.container.classList.remove(this.classNames.resizing);
5242
+ }
5243
+ document.removeEventListener("mousemove", this.handleMouseMove);
5244
+ document.removeEventListener("mouseup", this.handleMouseUp);
5245
+ document.removeEventListener("keydown", this.handleKeyDown);
5246
+ document.removeEventListener("keyup", this.handleKeyUp);
5247
+ };
5248
+ /**
5249
+ * Tracks Shift key state to enable temporary aspect ratio locking.
5250
+ *
5251
+ * When Shift is pressed during resize, aspect ratio is preserved even if
5252
+ * preserveAspectRatio is false.
5253
+ *
5254
+ * @param event - The keyboard event
5255
+ */
5256
+ this.handleKeyDown = (event) => {
5257
+ if (event.key === "Shift") {
5258
+ this.isShiftKeyPressed = true;
5259
+ }
5260
+ };
5261
+ /**
5262
+ * Tracks Shift key release to disable temporary aspect ratio locking.
5263
+ *
5264
+ * @param event - The keyboard event
5265
+ */
5266
+ this.handleKeyUp = (event) => {
5267
+ if (event.key === "Shift") {
5268
+ this.isShiftKeyPressed = false;
5269
+ }
5270
+ };
5271
+ var _a, _b, _c, _d, _e;
5272
+ this.node = options.node;
5273
+ this.element = options.element;
5274
+ this.contentElement = options.contentElement;
5275
+ this.getPos = options.getPos;
5276
+ this.onResize = options.onResize;
5277
+ this.onCommit = options.onCommit;
5278
+ this.onUpdate = options.onUpdate;
5279
+ if ((_a = options.options) == null ? void 0 : _a.min) {
5280
+ this.minSize = {
5281
+ ...this.minSize,
5282
+ ...options.options.min
5283
+ };
5284
+ }
5285
+ if ((_b = options.options) == null ? void 0 : _b.max) {
5286
+ this.maxSize = options.options.max;
5287
+ }
5288
+ if ((_c = options == null ? void 0 : options.options) == null ? void 0 : _c.directions) {
5289
+ this.directions = options.options.directions;
5290
+ }
5291
+ if ((_d = options.options) == null ? void 0 : _d.preserveAspectRatio) {
5292
+ this.preserveAspectRatio = options.options.preserveAspectRatio;
5293
+ }
5294
+ if ((_e = options.options) == null ? void 0 : _e.className) {
5295
+ this.classNames = {
5296
+ container: options.options.className.container || "",
5297
+ wrapper: options.options.className.wrapper || "",
5298
+ handle: options.options.className.handle || "",
5299
+ resizing: options.options.className.resizing || ""
5300
+ };
5301
+ }
5302
+ this.wrapper = this.createWrapper();
5303
+ this.container = this.createContainer();
5304
+ this.applyInitialSize();
5305
+ this.attachHandles();
5306
+ }
5307
+ /**
5308
+ * Returns the top-level DOM node that should be placed in the editor.
5309
+ *
5310
+ * This is required by the ProseMirror NodeView interface. The container
5311
+ * includes the wrapper, handles, and the actual content element.
5312
+ *
5313
+ * @returns The container element to be inserted into the editor
5314
+ */
5315
+ get dom() {
5316
+ return this.container;
5317
+ }
5318
+ get contentDOM() {
5319
+ return this.contentElement;
5320
+ }
5321
+ /**
5322
+ * Called when the node's content or attributes change.
5323
+ *
5324
+ * Updates the internal node reference. If a custom `onUpdate` callback
5325
+ * was provided, it will be called to handle additional update logic.
5326
+ *
5327
+ * @param node - The new/updated node
5328
+ * @param decorations - Node decorations
5329
+ * @param innerDecorations - Inner decorations
5330
+ * @returns `false` if the node type has changed (requires full rebuild), otherwise the result of `onUpdate` or `true`
5331
+ */
5332
+ update(node, decorations, innerDecorations) {
5333
+ if (node.type !== this.node.type) {
5334
+ return false;
5335
+ }
5336
+ this.node = node;
5337
+ if (this.onUpdate) {
5338
+ return this.onUpdate(node, decorations, innerDecorations);
5339
+ }
5340
+ return true;
5341
+ }
5342
+ /**
5343
+ * Cleanup method called when the node view is being removed.
5344
+ *
5345
+ * Removes all event listeners to prevent memory leaks. This is required
5346
+ * by the ProseMirror NodeView interface. If a resize is active when
5347
+ * destroy is called, it will be properly cancelled.
5348
+ */
5349
+ destroy() {
5350
+ if (this.isResizing) {
5351
+ this.container.dataset.resizeState = "false";
5352
+ if (this.classNames.resizing) {
5353
+ this.container.classList.remove(this.classNames.resizing);
5354
+ }
5355
+ document.removeEventListener("mousemove", this.handleMouseMove);
5356
+ document.removeEventListener("mouseup", this.handleMouseUp);
5357
+ document.removeEventListener("keydown", this.handleKeyDown);
5358
+ document.removeEventListener("keyup", this.handleKeyUp);
5359
+ this.isResizing = false;
5360
+ this.activeHandle = null;
5361
+ }
5362
+ this.container.remove();
5363
+ }
5364
+ /**
5365
+ * Creates the outer container element.
5366
+ *
5367
+ * The container is the top-level element returned by the NodeView and
5368
+ * wraps the entire resizable node. It's set up with flexbox to handle
5369
+ * alignment and includes data attributes for styling and identification.
5370
+ *
5371
+ * @returns The container element
5372
+ */
5373
+ createContainer() {
5374
+ const element = document.createElement("div");
5375
+ element.dataset.resizeContainer = "";
5376
+ element.dataset.node = this.node.type.name;
5377
+ element.style.display = "flex";
5378
+ element.style.justifyContent = "flex-start";
5379
+ element.style.alignItems = "flex-start";
5380
+ if (this.classNames.container) {
5381
+ element.className = this.classNames.container;
5382
+ }
5383
+ element.appendChild(this.wrapper);
5384
+ return element;
5385
+ }
5386
+ /**
5387
+ * Creates the wrapper element that contains the content and handles.
5388
+ *
5389
+ * The wrapper uses relative positioning so that resize handles can be
5390
+ * positioned absolutely within it. This is the direct parent of the
5391
+ * content element being made resizable.
5392
+ *
5393
+ * @returns The wrapper element
5394
+ */
5395
+ createWrapper() {
5396
+ const element = document.createElement("div");
5397
+ element.style.position = "relative";
5398
+ element.style.display = "block";
5399
+ element.dataset.resizeWrapper = "";
5400
+ if (this.classNames.wrapper) {
5401
+ element.className = this.classNames.wrapper;
5402
+ }
5403
+ element.appendChild(this.element);
5404
+ return element;
5405
+ }
5406
+ /**
5407
+ * Creates a resize handle element for a specific direction.
5408
+ *
5409
+ * Each handle is absolutely positioned and includes a data attribute
5410
+ * identifying its direction for styling purposes.
5411
+ *
5412
+ * @param direction - The resize direction for this handle
5413
+ * @returns The handle element
5414
+ */
5415
+ createHandle(direction) {
5416
+ const handle = document.createElement("div");
5417
+ handle.dataset.resizeHandle = direction;
5418
+ handle.style.position = "absolute";
5419
+ if (this.classNames.handle) {
5420
+ handle.className = this.classNames.handle;
5421
+ }
5422
+ return handle;
5423
+ }
5424
+ /**
5425
+ * Positions a handle element according to its direction.
5426
+ *
5427
+ * Corner handles (e.g., 'top-left') are positioned at the intersection
5428
+ * of two edges. Edge handles (e.g., 'top') span the full width or height.
5429
+ *
5430
+ * @param handle - The handle element to position
5431
+ * @param direction - The direction determining the position
5432
+ */
5433
+ positionHandle(handle, direction) {
5434
+ const isTop = direction.includes("top");
5435
+ const isBottom = direction.includes("bottom");
5436
+ const isLeft = direction.includes("left");
5437
+ const isRight = direction.includes("right");
5438
+ if (isTop) {
5439
+ handle.style.top = "0";
5440
+ }
5441
+ if (isBottom) {
5442
+ handle.style.bottom = "0";
5443
+ }
5444
+ if (isLeft) {
5445
+ handle.style.left = "0";
5446
+ }
5447
+ if (isRight) {
5448
+ handle.style.right = "0";
5449
+ }
5450
+ if (direction === "top" || direction === "bottom") {
5451
+ handle.style.left = "0";
5452
+ handle.style.right = "0";
5453
+ }
5454
+ if (direction === "left" || direction === "right") {
5455
+ handle.style.top = "0";
5456
+ handle.style.bottom = "0";
5457
+ }
5458
+ }
5459
+ /**
5460
+ * Creates and attaches all resize handles to the wrapper.
5461
+ *
5462
+ * Iterates through the configured directions, creates a handle for each,
5463
+ * positions it, attaches the mousedown listener, and appends it to the DOM.
5464
+ */
5465
+ attachHandles() {
5466
+ this.directions.forEach((direction) => {
5467
+ const handle = this.createHandle(direction);
5468
+ this.positionHandle(handle, direction);
5469
+ handle.addEventListener("mousedown", (event) => this.handleResizeStart(event, direction));
5470
+ handle.addEventListener("touchstart", (event) => this.handleResizeStart(event, direction));
5471
+ this.wrapper.appendChild(handle);
5472
+ });
5473
+ }
5474
+ /**
5475
+ * Applies initial sizing from node attributes to the element.
5476
+ *
5477
+ * If width/height attributes exist on the node, they're applied to the element.
5478
+ * Otherwise, the element's natural/current dimensions are measured. The aspect
5479
+ * ratio is calculated for later use in aspect-ratio-preserving resizes.
5480
+ */
5481
+ applyInitialSize() {
5482
+ const width = this.node.attrs.width;
5483
+ const height = this.node.attrs.height;
5484
+ if (width) {
5485
+ this.element.style.width = `${width}px`;
5486
+ this.initialWidth = width;
5487
+ } else {
5488
+ this.initialWidth = this.element.offsetWidth;
5489
+ }
5490
+ if (height) {
5491
+ this.element.style.height = `${height}px`;
5492
+ this.initialHeight = height;
5493
+ } else {
5494
+ this.initialHeight = this.element.offsetHeight;
5495
+ }
5496
+ if (this.initialWidth > 0 && this.initialHeight > 0) {
5497
+ this.aspectRatio = this.initialWidth / this.initialHeight;
5498
+ }
5499
+ }
5500
+ /**
5501
+ * Initiates a resize operation when a handle is clicked.
5502
+ *
5503
+ * Captures the starting mouse position and element dimensions, sets up
5504
+ * the resize state, adds the resizing class and state attribute, and
5505
+ * attaches document-level listeners for mouse movement and keyboard input.
5506
+ *
5507
+ * @param event - The mouse down event
5508
+ * @param direction - The direction of the handle being dragged
5509
+ */
5510
+ handleResizeStart(event, direction) {
5511
+ event.preventDefault();
5512
+ event.stopPropagation();
5513
+ this.isResizing = true;
5514
+ this.activeHandle = direction;
5515
+ if (isTouchEvent(event)) {
5516
+ this.startX = event.touches[0].clientX;
5517
+ this.startY = event.touches[0].clientY;
5518
+ } else {
5519
+ this.startX = event.clientX;
5520
+ this.startY = event.clientY;
5521
+ }
5522
+ this.startWidth = this.element.offsetWidth;
5523
+ this.startHeight = this.element.offsetHeight;
5524
+ if (this.startWidth > 0 && this.startHeight > 0) {
5525
+ this.aspectRatio = this.startWidth / this.startHeight;
5526
+ }
5527
+ const pos = this.getPos();
5528
+ if (pos !== void 0) {
5529
+ }
5530
+ this.container.dataset.resizeState = "true";
5531
+ if (this.classNames.resizing) {
5532
+ this.container.classList.add(this.classNames.resizing);
5533
+ }
5534
+ document.addEventListener("mousemove", this.handleMouseMove);
5535
+ document.addEventListener("touchmove", this.handleTouchMove);
5536
+ document.addEventListener("mouseup", this.handleMouseUp);
5537
+ document.addEventListener("keydown", this.handleKeyDown);
5538
+ document.addEventListener("keyup", this.handleKeyUp);
5539
+ }
5540
+ handleResize(deltaX, deltaY) {
5541
+ if (!this.activeHandle) {
5542
+ return;
5543
+ }
5544
+ const shouldPreserveAspectRatio = this.preserveAspectRatio || this.isShiftKeyPressed;
5545
+ const { width, height } = this.calculateNewDimensions(this.activeHandle, deltaX, deltaY);
5546
+ const constrained = this.applyConstraints(width, height, shouldPreserveAspectRatio);
5547
+ this.element.style.width = `${constrained.width}px`;
5548
+ this.element.style.height = `${constrained.height}px`;
5549
+ if (this.onResize) {
5550
+ this.onResize(constrained.width, constrained.height);
5551
+ }
5552
+ }
5553
+ /**
5554
+ * Calculates new dimensions based on mouse delta and resize direction.
5555
+ *
5556
+ * Takes the starting dimensions and applies the mouse movement delta
5557
+ * according to the handle direction. For corner handles, both dimensions
5558
+ * are affected. For edge handles, only one dimension changes. If aspect
5559
+ * ratio should be preserved, delegates to applyAspectRatio.
5560
+ *
5561
+ * @param direction - The active resize handle direction
5562
+ * @param deltaX - Horizontal mouse movement since resize start
5563
+ * @param deltaY - Vertical mouse movement since resize start
5564
+ * @returns The calculated width and height
5565
+ */
5566
+ calculateNewDimensions(direction, deltaX, deltaY) {
5567
+ let newWidth = this.startWidth;
5568
+ let newHeight = this.startHeight;
5569
+ const isRight = direction.includes("right");
5570
+ const isLeft = direction.includes("left");
5571
+ const isBottom = direction.includes("bottom");
5572
+ const isTop = direction.includes("top");
5573
+ if (isRight) {
5574
+ newWidth = this.startWidth + deltaX;
5575
+ } else if (isLeft) {
5576
+ newWidth = this.startWidth - deltaX;
5577
+ }
5578
+ if (isBottom) {
5579
+ newHeight = this.startHeight + deltaY;
5580
+ } else if (isTop) {
5581
+ newHeight = this.startHeight - deltaY;
5582
+ }
5583
+ if (direction === "right" || direction === "left") {
5584
+ newWidth = this.startWidth + (isRight ? deltaX : -deltaX);
5585
+ }
5586
+ if (direction === "top" || direction === "bottom") {
5587
+ newHeight = this.startHeight + (isBottom ? deltaY : -deltaY);
5588
+ }
5589
+ const shouldPreserveAspectRatio = this.preserveAspectRatio || this.isShiftKeyPressed;
5590
+ if (shouldPreserveAspectRatio) {
5591
+ return this.applyAspectRatio(newWidth, newHeight, direction);
5592
+ }
5593
+ return { width: newWidth, height: newHeight };
5594
+ }
5595
+ /**
5596
+ * Applies min/max constraints to dimensions.
5597
+ *
5598
+ * When aspect ratio is NOT preserved, constraints are applied independently
5599
+ * to width and height. When aspect ratio IS preserved, constraints are
5600
+ * applied while maintaining the aspect ratio—if one dimension hits a limit,
5601
+ * the other is recalculated proportionally.
5602
+ *
5603
+ * This ensures that aspect ratio is never broken when constrained.
5604
+ *
5605
+ * @param width - The unconstrained width
5606
+ * @param height - The unconstrained height
5607
+ * @param preserveAspectRatio - Whether to maintain aspect ratio while constraining
5608
+ * @returns The constrained dimensions
5609
+ */
5610
+ applyConstraints(width, height, preserveAspectRatio) {
5611
+ var _a, _b, _c, _d;
5612
+ if (!preserveAspectRatio) {
5613
+ let constrainedWidth2 = Math.max(this.minSize.width, width);
5614
+ let constrainedHeight2 = Math.max(this.minSize.height, height);
5615
+ if ((_a = this.maxSize) == null ? void 0 : _a.width) {
5616
+ constrainedWidth2 = Math.min(this.maxSize.width, constrainedWidth2);
5617
+ }
5618
+ if ((_b = this.maxSize) == null ? void 0 : _b.height) {
5619
+ constrainedHeight2 = Math.min(this.maxSize.height, constrainedHeight2);
5620
+ }
5621
+ return { width: constrainedWidth2, height: constrainedHeight2 };
5622
+ }
5623
+ let constrainedWidth = width;
5624
+ let constrainedHeight = height;
5625
+ if (constrainedWidth < this.minSize.width) {
5626
+ constrainedWidth = this.minSize.width;
5627
+ constrainedHeight = constrainedWidth / this.aspectRatio;
5628
+ }
5629
+ if (constrainedHeight < this.minSize.height) {
5630
+ constrainedHeight = this.minSize.height;
5631
+ constrainedWidth = constrainedHeight * this.aspectRatio;
5632
+ }
5633
+ if (((_c = this.maxSize) == null ? void 0 : _c.width) && constrainedWidth > this.maxSize.width) {
5634
+ constrainedWidth = this.maxSize.width;
5635
+ constrainedHeight = constrainedWidth / this.aspectRatio;
5636
+ }
5637
+ if (((_d = this.maxSize) == null ? void 0 : _d.height) && constrainedHeight > this.maxSize.height) {
5638
+ constrainedHeight = this.maxSize.height;
5639
+ constrainedWidth = constrainedHeight * this.aspectRatio;
5640
+ }
5641
+ return { width: constrainedWidth, height: constrainedHeight };
5642
+ }
5643
+ /**
5644
+ * Adjusts dimensions to maintain the original aspect ratio.
5645
+ *
5646
+ * For horizontal handles (left/right), uses width as the primary dimension
5647
+ * and calculates height from it. For vertical handles (top/bottom), uses
5648
+ * height as primary and calculates width. For corner handles, uses width
5649
+ * as the primary dimension.
5650
+ *
5651
+ * @param width - The new width
5652
+ * @param height - The new height
5653
+ * @param direction - The active resize direction
5654
+ * @returns Dimensions adjusted to preserve aspect ratio
5655
+ */
5656
+ applyAspectRatio(width, height, direction) {
5657
+ const isHorizontal = direction === "left" || direction === "right";
5658
+ const isVertical = direction === "top" || direction === "bottom";
5659
+ if (isHorizontal) {
5660
+ return {
5661
+ width,
5662
+ height: width / this.aspectRatio
5663
+ };
5664
+ }
5665
+ if (isVertical) {
5666
+ return {
5667
+ width: height * this.aspectRatio,
5668
+ height
5669
+ };
5670
+ }
5671
+ return {
5672
+ width,
5673
+ height: width / this.aspectRatio
5674
+ };
5675
+ }
5676
+ };
5677
+ var ResizableNodeview = ResizableNodeView;
5678
+
5139
5679
  // src/utilities/canInsertNode.ts
5140
5680
  var import_state22 = require("@tiptap/pm/state");
5141
5681
  function canInsertNode(state, nodeType) {
@@ -6087,6 +6627,8 @@ var Tracker = class {
6087
6627
  NodePos,
6088
6628
  NodeView,
6089
6629
  PasteRule,
6630
+ ResizableNodeView,
6631
+ ResizableNodeview,
6090
6632
  Tracker,
6091
6633
  callOrReturn,
6092
6634
  canInsertNode,