@pentestpad/tiptap-extension-figure 1.0.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 (32) hide show
  1. package/README.md +39 -0
  2. package/dist/component/tip-tap-image-resize-with-caption.d.ts +5 -0
  3. package/dist/index.cjs.js +452 -0
  4. package/dist/index.cjs.js.map +1 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +448 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/utils/add-caption-controls.util.d.ts +1 -0
  9. package/dist/utils/add-image-alignment-controls.util.d.ts +1 -0
  10. package/dist/utils/add-image-resize-controls.util.d.ts +1 -0
  11. package/dist/utils/is-mobile-screen.util.d.ts +1 -0
  12. package/dist/utils/remove-image-controls.util.d.ts +1 -0
  13. package/dist/utils/replace-element.util.d.ts +2 -0
  14. package/package.json +33 -0
  15. package/rollup.config.js +48 -0
  16. package/src/assets/icons/closed-caption-add.svg +1 -0
  17. package/src/assets/icons/delete.svg +1 -0
  18. package/src/assets/icons/format-align-center.svg +1 -0
  19. package/src/assets/icons/format-align-left.svg +1 -0
  20. package/src/assets/icons/format-align-right.svg +1 -0
  21. package/src/assets/icons/svg.d.ts +4 -0
  22. package/src/assets/styles/styles.css +85 -0
  23. package/src/assets/styles/styles.d.ts +4 -0
  24. package/src/component/tip-tap-image-resize-with-caption.ts +300 -0
  25. package/src/index.ts +3 -0
  26. package/src/utils/add-caption-controls.util.ts +51 -0
  27. package/src/utils/add-image-alignment-controls.util.ts +62 -0
  28. package/src/utils/add-image-resize-controls.util.ts +104 -0
  29. package/src/utils/is-mobile-screen.util.ts +1 -0
  30. package/src/utils/remove-image-controls.util.ts +22 -0
  31. package/src/utils/replace-element.util.ts +44 -0
  32. package/tsconfig.json +18 -0
package/dist/index.js ADDED
@@ -0,0 +1,448 @@
1
+ import { nodeInputRule, mergeAttributes } from '@tiptap/core';
2
+ import ImageExtension from '@tiptap/extension-image';
3
+
4
+ const isMobileScreen = () => document.documentElement.clientWidth < 768;
5
+
6
+ const removeImageControlsAndResetStyles = (clickedElement, wrapperElement, styles) => {
7
+ const containerContainsClickedElement = wrapperElement.contains(clickedElement);
8
+ if (containerContainsClickedElement) {
9
+ return;
10
+ }
11
+ // Remove all custom UI elements and styling
12
+ wrapperElement.classList.remove(styles["active"]);
13
+ const children = Array.from(wrapperElement.children);
14
+ children.forEach((child) => {
15
+ if (child.tagName !== "IMG" && child.tagName !== "FIGCAPTION") {
16
+ wrapperElement.removeChild(child);
17
+ }
18
+ });
19
+ };
20
+
21
+ var leftIcon = "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2220px%22%20viewBox%3D%220%20-960%20960%20960%22%20width%3D%2220px%22%20fill%3D%22%23000000%22%3E%3Cpath%20d%3D%22M144-144v-72h672v72H144Zm0-150v-72h480v72H144Zm0-150v-72h672v72H144Zm0-150v-72h480v72H144Zm0-150v-72h672v72H144Z%22%2F%3E%3C%2Fsvg%3E";
22
+
23
+ var centerIcon = "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2220px%22%20viewBox%3D%220%20-960%20960%20960%22%20width%3D%2220px%22%20fill%3D%22%23000000%22%3E%3Cpath%20d%3D%22M144-144v-72h672v72H144Zm144-150v-72h384v72H288ZM144-444v-72h672v72H144Zm144-150v-72h384v72H288ZM144-744v-72h672v72H144Z%22%2F%3E%3C%2Fsvg%3E";
24
+
25
+ var rightIcon = "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2220px%22%20viewBox%3D%220%20-960%20960%20960%22%20width%3D%2220px%22%20fill%3D%22%23000000%22%3E%3Cpath%20d%3D%22M144-744v-72h672v72H144Zm192%20150v-72h480v72H336ZM144-444v-72h672v72H144Zm192%20150v-72h480v72H336ZM144-144v-72h672v72H144Z%22%2F%3E%3C%2Fsvg%3E";
26
+
27
+ const imageAlignmentControls = [
28
+ {
29
+ type: "left",
30
+ icon: leftIcon,
31
+ styleToApply: "margin: 0 auto 0 0;",
32
+ },
33
+ {
34
+ type: "center",
35
+ icon: centerIcon,
36
+ styleToApply: "margin: 0 auto;",
37
+ },
38
+ {
39
+ type: "right",
40
+ icon: rightIcon,
41
+ styleToApply: "margin: 0 0 0 auto;",
42
+ },
43
+ ];
44
+ const addImageAlignmentControls = (wrapperElement, imageElement, styles, onAlign) => {
45
+ const imageAlignmentContainer = document.createElement("div");
46
+ imageAlignmentContainer.setAttribute("contenteditable", "false");
47
+ imageAlignmentContainer.setAttribute("class", styles["image-alignment-container"]);
48
+ imageAlignmentControls.forEach((imageControl) => {
49
+ const imageAlignmentControl = document.createElement("img");
50
+ imageAlignmentControl.src = imageControl.icon;
51
+ imageAlignmentControl.setAttribute("class", styles["image-alignment-control"]);
52
+ imageAlignmentControl.addEventListener("click", (event) => {
53
+ event.stopPropagation();
54
+ imageElement.style.cssText = `${imageElement.style.cssText} ${imageControl.styleToApply}`;
55
+ onAlign();
56
+ });
57
+ imageAlignmentContainer.appendChild(imageAlignmentControl);
58
+ });
59
+ wrapperElement.appendChild(imageAlignmentContainer);
60
+ };
61
+
62
+ const addImageResizeControls = (wrapperElement, imageElement, isResizing, startX, startWidth, styles, onResize) => {
63
+ const isMobile = isMobileScreen();
64
+ const dotPosition = isMobile ? "-8px" : "-4px";
65
+ const dotSize = isMobile ? 16 : 9;
66
+ const dotsPosition = [
67
+ `top: ${dotPosition}; left: ${dotPosition}; cursor: nwse-resize;`,
68
+ `top: ${dotPosition}; right: ${dotPosition}; cursor: nesw-resize;`,
69
+ `bottom: ${dotPosition}; left: ${dotPosition}; cursor: nesw-resize;`,
70
+ `bottom: ${dotPosition}; right: ${dotPosition}; cursor: nwse-resize;`,
71
+ ];
72
+ Array.from({ length: 4 }, (_, index) => {
73
+ const dotElement = document.createElement("div");
74
+ dotElement.setAttribute("class", styles["dot-element"]);
75
+ dotElement.setAttribute("style", `width: ${dotSize}px; height: ${dotSize}px; ${dotsPosition[index]}`);
76
+ dotElement.addEventListener("mousedown", (e) => {
77
+ e.preventDefault();
78
+ isResizing = true;
79
+ startX = e.clientX;
80
+ startWidth = wrapperElement.offsetWidth;
81
+ const onMouseMove = (event) => {
82
+ if (!isResizing) {
83
+ return;
84
+ }
85
+ const deltaX = index % 2 === 0 ? -(event.clientX - startX) : event.clientX - startX;
86
+ const newWidth = startWidth + deltaX;
87
+ wrapperElement.style.width = newWidth + "px";
88
+ imageElement.style.width = newWidth + "px";
89
+ };
90
+ const onMouseUp = () => {
91
+ if (isResizing) {
92
+ isResizing = false;
93
+ }
94
+ onResize();
95
+ document.removeEventListener("mousemove", onMouseMove);
96
+ document.removeEventListener("mouseup", onMouseUp);
97
+ };
98
+ document.addEventListener("mousemove", onMouseMove);
99
+ document.addEventListener("mouseup", onMouseUp);
100
+ });
101
+ dotElement.addEventListener("touchstart", (e) => {
102
+ e.cancelable && e.preventDefault();
103
+ isResizing = true;
104
+ startX = e.touches[0].clientX;
105
+ startWidth = wrapperElement.offsetWidth;
106
+ const onTouchMove = (e) => {
107
+ if (!isResizing)
108
+ return;
109
+ const deltaX = index % 2 === 0
110
+ ? -(e.touches[0].clientX - startX)
111
+ : e.touches[0].clientX - startX;
112
+ const newWidth = startWidth + deltaX;
113
+ wrapperElement.style.width = newWidth + "px";
114
+ imageElement.style.width = newWidth + "px";
115
+ };
116
+ const onTouchEnd = () => {
117
+ if (isResizing) {
118
+ isResizing = false;
119
+ }
120
+ onResize();
121
+ document.removeEventListener("touchmove", onTouchMove);
122
+ document.removeEventListener("touchend", onTouchEnd);
123
+ };
124
+ document.addEventListener("touchmove", onTouchMove);
125
+ document.addEventListener("touchend", onTouchEnd);
126
+ }, { passive: false });
127
+ wrapperElement.appendChild(dotElement);
128
+ });
129
+ };
130
+
131
+ var closedCaptionAddIcon = "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2220px%22%20viewBox%3D%220%20-960%20960%20960%22%20width%3D%2220px%22%20fill%3D%22%23000000%22%3E%3Cpath%20d%3D%22M480-480Zm120%20288H216q-29.7%200-50.85-21.16Q144-234.32%20144-264.04v-432.24Q144-726%20165.15-747T216-768h528q29.7%200%2050.85%2021.15Q816-725.7%20816-696v288h-72v-288H216v432h384v72Zm144%2072v-72h-72v-72h72v-72h72v72h72v72h-72v72h-72ZM293.29-368h111.86Q421-368%20432-378.78q11-10.78%2011-26.72V-443h-56.14v19H312v-112h75v19h56v-37.89q0-16.11-10.64-26.61Q421.73-592%20406-592H293.01q-16.01%200-26.51%2010.71-10.5%2010.7-10.5%2026.52v148.95Q256-390%20266.72-379t26.57%2011Zm261.22%200h112.55q15.94%200%2026.44-10.78Q704-389.56%20704-405.5V-443h-56.14v19H573v-112h75v19h56v-37.89q0-16.11-10.72-26.61T666.71-592H554.85Q539-592%20528-581.29q-11%2010.7-11%2026.52v148.95Q517-390%20527.79-379q10.78%2011%2026.72%2011Z%22%2F%3E%3C%2Fsvg%3E";
132
+
133
+ var deleteIcon = "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2220px%22%20viewBox%3D%220%20-960%20960%20960%22%20width%3D%2220px%22%20fill%3D%22%23000000%22%3E%3Cpath%20d%3D%22M312-144q-29.7%200-50.85-21.15Q240-186.3%20240-216v-480h-48v-72h192v-48h192v48h192v72h-48v479.57Q720-186%20698.85-165T648-144H312Zm336-552H312v480h336v-480ZM384-288h72v-336h-72v336Zm120%200h72v-336h-72v336ZM312-696v480-480Z%22%2F%3E%3C%2Fsvg%3E";
134
+
135
+ const addCaptionControls = (wrapperElement, styles, onCaptionRemove, onCaptionAdd) => {
136
+ const captionControlsContainer = document.createElement("div");
137
+ captionControlsContainer.setAttribute("contenteditable", "false");
138
+ captionControlsContainer.setAttribute("class", styles["caption-controls-element"]);
139
+ // If wrapper element is a figure and the button doesn't already exist, add a button to remove caption
140
+ // Also, wrapper elements needs to become a div
141
+ if (wrapperElement.tagName === "FIGURE" &&
142
+ !wrapperElement.querySelector(styles["remove-caption-button"])) {
143
+ const removeCaptionButton = document.createElement("img");
144
+ removeCaptionButton.src = deleteIcon;
145
+ removeCaptionButton.setAttribute("class", styles["remove-caption-button"]);
146
+ removeCaptionButton.addEventListener("click", (event) => {
147
+ event.stopPropagation();
148
+ onCaptionRemove();
149
+ });
150
+ captionControlsContainer.appendChild(removeCaptionButton);
151
+ wrapperElement.appendChild(captionControlsContainer);
152
+ return;
153
+ }
154
+ // If wrapper element is a div and the button doesn't already exist, add a button to add caption
155
+ if (wrapperElement.tagName === "DIV" &&
156
+ !wrapperElement.querySelector(styles["add-caption-button"])) {
157
+ const addCaptionButton = document.createElement("img");
158
+ addCaptionButton.src = closedCaptionAddIcon;
159
+ addCaptionButton.setAttribute("class", styles["add-caption-button"]);
160
+ addCaptionButton.addEventListener("click", (event) => {
161
+ event.stopPropagation();
162
+ onCaptionAdd();
163
+ });
164
+ captionControlsContainer.appendChild(addCaptionButton);
165
+ wrapperElement.appendChild(captionControlsContainer);
166
+ }
167
+ };
168
+
169
+ const changeFigureToImage = (wrapperElement) => {
170
+ const imageWrapperElement = document.createElement("div");
171
+ const oldAttributes = wrapperElement.attributes;
172
+ const newAttributes = imageWrapperElement.attributes;
173
+ // Copy attributes
174
+ for (let i = 0, len = oldAttributes.length; i < len; i++) {
175
+ newAttributes.setNamedItem(oldAttributes.item(i).cloneNode());
176
+ }
177
+ // Find the image within the old wrapper and set it as the only child of the new wrapper
178
+ const imageElement = wrapperElement.querySelector("img");
179
+ if (imageElement) {
180
+ imageWrapperElement.appendChild(imageElement);
181
+ }
182
+ // Replace wrapperElement with imageWrapperElement
183
+ wrapperElement.replaceWith(imageWrapperElement);
184
+ };
185
+ const changeImageToFigure = (wrapperElement, captionElement) => {
186
+ const figureWrapperElement = document.createElement("figure");
187
+ const oldAttributes = wrapperElement.attributes;
188
+ const newAttributes = figureWrapperElement.attributes;
189
+ // Copy attributes
190
+ for (let i = 0, len = oldAttributes.length; i < len; i++) {
191
+ newAttributes.setNamedItem(oldAttributes.item(i).cloneNode());
192
+ }
193
+ // Find the image within the old wrapper and set it as the only child of the new wrapper
194
+ const imageElement = wrapperElement.querySelector("img");
195
+ if (imageElement) {
196
+ figureWrapperElement.appendChild(imageElement);
197
+ }
198
+ captionElement.innerHTML = "Caption";
199
+ figureWrapperElement.appendChild(captionElement);
200
+ // Replace wrapperElement with figureWrapperElement
201
+ wrapperElement.replaceWith(figureWrapperElement);
202
+ };
203
+
204
+ function styleInject(css, ref) {
205
+ if ( ref === void 0 ) ref = {};
206
+ var insertAt = ref.insertAt;
207
+
208
+ if (typeof document === 'undefined') { return; }
209
+
210
+ var head = document.head || document.getElementsByTagName('head')[0];
211
+ var style = document.createElement('style');
212
+ style.type = 'text/css';
213
+
214
+ if (insertAt === 'top') {
215
+ if (head.firstChild) {
216
+ head.insertBefore(style, head.firstChild);
217
+ } else {
218
+ head.appendChild(style);
219
+ }
220
+ } else {
221
+ head.appendChild(style);
222
+ }
223
+
224
+ if (style.styleSheet) {
225
+ style.styleSheet.cssText = css;
226
+ } else {
227
+ style.appendChild(document.createTextNode(css));
228
+ }
229
+ }
230
+
231
+ var css_248z = ".styles_wrapper-element__SoyDK {\n display: flex;\n flex-direction: column;\n position: relative;\n cursor: pointer;\n width: fit-content;\n}\n.styles_wrapper-element__SoyDK.styles_active__kXAaT {\n border: 2px dashed #6c6c6c;\n}\n\n.styles_figure-element__wBqOu {\n}\n\n.styles_caption-element__-9Bt- {\n text-align: center;\n margin-top: 8px;\n min-height: 1em;\n margin: 0.5rem 2rem;\n padding: 0.5rem 0;\n}\n.styles_caption-element__-9Bt-:hover {\n border-radius: 4px;\n border: 2px dashed #6c6c6c;\n}\n\n.styles_caption-controls-element__Pwjxq {\n position: absolute;\n bottom: 7.5%;\n left: 50%;\n width: 40px;\n height: 35px;\n z-index: 999;\n background-color: rgba(255, 255, 255, 0.7);\n border-radius: 4px;\n border: 2px solid #6c6c6c;\n cursor: pointer;\n transform: translate(-50%, -50%);\n display: flex;\n justify-content: center;\n align-items: center;\n}\n\n.styles_remove-caption-button__OzMEn,\n.styles_add-caption-button__2rKuu {\n cursor: pointer;\n font-size: 20px;\n}\n\n.styles_remove-caption-button__OzMEn:hover,\n.styles_add-caption-button__2rKuu:hover {\n opacity: 0.5;\n}\n\n.styles_dot-element__TQRBe {\n position: absolute;\n border: 1.5px solid #6c6c6c;\n border-radius: 50%;\n}\n\n.styles_image-alignment-container__5byQ2 {\n position: absolute;\n top: 0%;\n left: 50%;\n width: 100px;\n height: 25px;\n z-index: 999;\n background-color: rgba(255, 255, 255, 0.7);\n border-radius: 4px;\n border: 2px solid #6c6c6c;\n cursor: pointer;\n transform: translate(-50%, -50%);\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 0 10px;\n}\n\n.styles_image-alignment-control__r3rTj {\n cursor: pointer;\n font-size: 20px;\n}\n.styles_image-alignment-control__r3rTj:hover {\n opacity: 0.5;\n}\n";
232
+ var styles = {"wrapper-element":"styles_wrapper-element__SoyDK","active":"styles_active__kXAaT","caption-element":"styles_caption-element__-9Bt-","caption-controls-element":"styles_caption-controls-element__Pwjxq","remove-caption-button":"styles_remove-caption-button__OzMEn","add-caption-button":"styles_add-caption-button__2rKuu","dot-element":"styles_dot-element__TQRBe","image-alignment-container":"styles_image-alignment-container__5byQ2","image-alignment-control":"styles_image-alignment-control__r3rTj"};
233
+ styleInject(css_248z);
234
+
235
+ const inputRegex = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/;
236
+ const TiptapImageFigureExtension = ImageExtension.extend({
237
+ addOptions() {
238
+ return {
239
+ ...this.parent?.(),
240
+ inline: false,
241
+ allowBase64: false,
242
+ HTMLAttributes: {},
243
+ };
244
+ },
245
+ group: "block",
246
+ draggable: true,
247
+ isolating: true,
248
+ content: "inline*",
249
+ addStorage() {
250
+ return {
251
+ elementsVisible: false,
252
+ currentActiveWrapper: null,
253
+ };
254
+ },
255
+ addAttributes() {
256
+ return {
257
+ ...this.parent?.(),
258
+ src: {
259
+ default: null,
260
+ parseHTML: (element) => {
261
+ if (element.tagName === "FIGURE") {
262
+ const img = element.querySelector("img");
263
+ return img?.getAttribute("src");
264
+ }
265
+ return element.getAttribute("src");
266
+ },
267
+ },
268
+ alt: {
269
+ default: null,
270
+ parseHTML: (element) => {
271
+ const img = element.querySelector("img");
272
+ return img?.getAttribute("alt");
273
+ },
274
+ },
275
+ title: {
276
+ default: null,
277
+ parseHTML: (element) => {
278
+ const img = element.querySelector("img");
279
+ return img?.getAttribute("title");
280
+ },
281
+ },
282
+ style: {
283
+ // This style is applied to the wrapper element
284
+ default: "width: 100%; height: auto; cursor: pointer;",
285
+ parseHTML: (element) => {
286
+ const width = element.getAttribute("width");
287
+ // const img = element.querySelector("img");
288
+ // const width = img?.getAttribute("width");
289
+ return width
290
+ ? `width: ${width}px; height: auto; cursor: pointer;`
291
+ : `${element.style.cssText}`;
292
+ },
293
+ },
294
+ };
295
+ },
296
+ parseHTML() {
297
+ return [
298
+ {
299
+ tag: "figure",
300
+ contentElement: "figcaption",
301
+ },
302
+ {
303
+ tag: this.options.allowBase64
304
+ ? "img[src]"
305
+ : 'img[src]:not([src^="data:"])',
306
+ },
307
+ ];
308
+ },
309
+ renderHTML({ HTMLAttributes, node }) {
310
+ const hasCaption = node.content.size > 0;
311
+ if (hasCaption) {
312
+ return [
313
+ "figure",
314
+ this.options.HTMLAttributes,
315
+ [
316
+ "img",
317
+ mergeAttributes(HTMLAttributes, {
318
+ draggable: false,
319
+ contenteditable: false,
320
+ }),
321
+ ],
322
+ ["figcaption", 0],
323
+ ];
324
+ }
325
+ return ["img", mergeAttributes(HTMLAttributes)];
326
+ },
327
+ addNodeView() {
328
+ return ({ node, editor, getPos }) => {
329
+ const dispatchNodeView = () => {
330
+ if (typeof getPos === "function") {
331
+ const newAttrs = {
332
+ ...node.attrs,
333
+ style: imageElement.style.cssText,
334
+ };
335
+ editor.view.dispatch(editor.view.state.tr.setNodeMarkup(getPos(), null, newAttrs));
336
+ this.storage.elementsVisible = false;
337
+ }
338
+ };
339
+ const { options: { editable }, } = editor;
340
+ const { style } = node.attrs;
341
+ // Create wrapper based on content
342
+ const wrapperElement = document.createElement(node.content.size > 0 ? "figure" : "div");
343
+ wrapperElement.setAttribute("class", styles["wrapper-element"]);
344
+ wrapperElement.setAttribute("style", style);
345
+ const imageElement = document.createElement("img");
346
+ wrapperElement.appendChild(imageElement);
347
+ const captionElement = document.createElement("figcaption");
348
+ // Set up image attributes
349
+ Object.entries(node.attrs).forEach(([key, value]) => {
350
+ if (value === undefined || value === null) {
351
+ return;
352
+ }
353
+ imageElement.setAttribute(key, value);
354
+ });
355
+ // Add caption if needed
356
+ if (node.content.size > 0) {
357
+ captionElement.setAttribute("class", styles["caption-element"]);
358
+ captionElement.setAttribute("contenteditable", "true");
359
+ wrapperElement.appendChild(captionElement);
360
+ }
361
+ if (!editable)
362
+ return { dom: wrapperElement, contentDOM: captionElement };
363
+ // Initialize control variables
364
+ let isResizing = false;
365
+ let startX = 0;
366
+ let startWidth = 0;
367
+ // Handle click on container
368
+ wrapperElement.addEventListener("click", (event) => {
369
+ event.stopPropagation();
370
+ event.preventDefault();
371
+ // If controls are already visible, check if another image or figure is being clicked on
372
+ if (this.storage.elementsVisible) {
373
+ const clickedElement = event.target;
374
+ const currentActiveWrapper = this.storage.currentActiveWrapper;
375
+ // Check if the clicked element is a child of the current active wrapper
376
+ // If it isn't, we must remove the controls from the previous wrapper and continue
377
+ // If it is, we do nothing
378
+ if (currentActiveWrapper &&
379
+ currentActiveWrapper.contains(clickedElement)) {
380
+ return;
381
+ }
382
+ if (currentActiveWrapper) {
383
+ removeImageControlsAndResetStyles(clickedElement, currentActiveWrapper, styles);
384
+ }
385
+ }
386
+ this.storage.currentActiveWrapper = wrapperElement;
387
+ const isMobile = isMobileScreen();
388
+ if (isMobile) {
389
+ const focusedElement = document.querySelector(".ProseMirror-focused");
390
+ focusedElement?.blur();
391
+ }
392
+ // Remove existing controls first
393
+ removeImageControlsAndResetStyles(event.target, wrapperElement, styles);
394
+ // Show new controls
395
+ wrapperElement.classList.toggle(styles["active"]);
396
+ addImageAlignmentControls(wrapperElement, imageElement, styles, () => {
397
+ dispatchNodeView();
398
+ editor.commands.focus();
399
+ });
400
+ addImageResizeControls(wrapperElement, imageElement, isResizing, startX, startWidth, styles, () => {
401
+ dispatchNodeView();
402
+ editor.commands.focus();
403
+ });
404
+ addCaptionControls(wrapperElement, styles, () => {
405
+ // On caption remove
406
+ changeFigureToImage(wrapperElement);
407
+ this.storage.elementsVisible = false;
408
+ }, () => {
409
+ // On caption add
410
+ changeImageToFigure(wrapperElement, captionElement);
411
+ this.storage.elementsVisible = false;
412
+ });
413
+ this.storage.elementsVisible = true;
414
+ });
415
+ // Handle clicks outside
416
+ document.addEventListener("click", (event) => {
417
+ if (!wrapperElement.contains(event.target)) {
418
+ removeImageControlsAndResetStyles(event.target, wrapperElement, styles);
419
+ this.storage.elementsVisible = false;
420
+ this.storage.currentActiveWrapper = null;
421
+ }
422
+ });
423
+ return {
424
+ dom: wrapperElement,
425
+ contentDOM: node.content.size > 0 ? captionElement : undefined,
426
+ ignoreMutation: (mutation) => {
427
+ // We must ignore mutations that happened outside the captionElement
428
+ return !captionElement.contains(mutation.target);
429
+ },
430
+ };
431
+ };
432
+ },
433
+ addInputRules() {
434
+ return [
435
+ nodeInputRule({
436
+ find: inputRegex,
437
+ type: this.type,
438
+ getAttributes: (match) => {
439
+ const [, alt, src, title] = match;
440
+ return { src, alt, title };
441
+ },
442
+ }),
443
+ ];
444
+ },
445
+ });
446
+
447
+ export { TiptapImageFigureExtension as default };
448
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/assets/icons/format-align-left.svg","../src/assets/icons/format-align-center.svg","../src/assets/icons/format-align-right.svg","../src/assets/icons/closed-caption-add.svg","../src/assets/icons/delete.svg","../node_modules/style-inject/dist/style-inject.es.js"],"sourcesContent":["export default \"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2220px%22%20viewBox%3D%220%20-960%20960%20960%22%20width%3D%2220px%22%20fill%3D%22%23000000%22%3E%3Cpath%20d%3D%22M144-144v-72h672v72H144Zm0-150v-72h480v72H144Zm0-150v-72h672v72H144Zm0-150v-72h480v72H144Zm0-150v-72h672v72H144Z%22%2F%3E%3C%2Fsvg%3E\"","export default \"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2220px%22%20viewBox%3D%220%20-960%20960%20960%22%20width%3D%2220px%22%20fill%3D%22%23000000%22%3E%3Cpath%20d%3D%22M144-144v-72h672v72H144Zm144-150v-72h384v72H288ZM144-444v-72h672v72H144Zm144-150v-72h384v72H288ZM144-744v-72h672v72H144Z%22%2F%3E%3C%2Fsvg%3E\"","export default \"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2220px%22%20viewBox%3D%220%20-960%20960%20960%22%20width%3D%2220px%22%20fill%3D%22%23000000%22%3E%3Cpath%20d%3D%22M144-744v-72h672v72H144Zm192%20150v-72h480v72H336ZM144-444v-72h672v72H144Zm192%20150v-72h480v72H336ZM144-144v-72h672v72H144Z%22%2F%3E%3C%2Fsvg%3E\"","export default \"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2220px%22%20viewBox%3D%220%20-960%20960%20960%22%20width%3D%2220px%22%20fill%3D%22%23000000%22%3E%3Cpath%20d%3D%22M480-480Zm120%20288H216q-29.7%200-50.85-21.16Q144-234.32%20144-264.04v-432.24Q144-726%20165.15-747T216-768h528q29.7%200%2050.85%2021.15Q816-725.7%20816-696v288h-72v-288H216v432h384v72Zm144%2072v-72h-72v-72h72v-72h72v72h72v72h-72v72h-72ZM293.29-368h111.86Q421-368%20432-378.78q11-10.78%2011-26.72V-443h-56.14v19H312v-112h75v19h56v-37.89q0-16.11-10.64-26.61Q421.73-592%20406-592H293.01q-16.01%200-26.51%2010.71-10.5%2010.7-10.5%2026.52v148.95Q256-390%20266.72-379t26.57%2011Zm261.22%200h112.55q15.94%200%2026.44-10.78Q704-389.56%20704-405.5V-443h-56.14v19H573v-112h75v19h56v-37.89q0-16.11-10.72-26.61T666.71-592H554.85Q539-592%20528-581.29q-11%2010.7-11%2026.52v148.95Q517-390%20527.79-379q10.78%2011%2026.72%2011Z%22%2F%3E%3C%2Fsvg%3E\"","export default \"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2220px%22%20viewBox%3D%220%20-960%20960%20960%22%20width%3D%2220px%22%20fill%3D%22%23000000%22%3E%3Cpath%20d%3D%22M312-144q-29.7%200-50.85-21.15Q240-186.3%20240-216v-480h-48v-72h192v-48h192v48h192v72h-48v479.57Q720-186%20698.85-165T648-144H312Zm336-552H312v480h336v-480ZM384-288h72v-336h-72v336Zm120%200h72v-336h-72v336ZM312-696v480-480Z%22%2F%3E%3C%2Fsvg%3E\"","function styleInject(css, ref) {\n if ( ref === void 0 ) ref = {};\n var insertAt = ref.insertAt;\n\n if (!css || typeof document === 'undefined') { return; }\n\n var head = document.head || document.getElementsByTagName('head')[0];\n var style = document.createElement('style');\n style.type = 'text/css';\n\n if (insertAt === 'top') {\n if (head.firstChild) {\n head.insertBefore(style, head.firstChild);\n } else {\n head.appendChild(style);\n }\n } else {\n head.appendChild(style);\n }\n\n if (style.styleSheet) {\n style.styleSheet.cssText = css;\n } else {\n style.appendChild(document.createTextNode(css));\n }\n}\n\nexport default styleInject;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA,eAAe;;ACAf,iBAAe;;ACAf,gBAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACAf,2BAAe;;ACAf,iBAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACAf,SAAS,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE;AAC/B,EAAE,KAAK,GAAG,KAAK,MAAM,GAAG,GAAG,GAAG,EAAE;AAChC,EAAE,IAAI,QAAQ,GAAG,GAAG,CAAC,QAAQ;;AAE7B,EAAE,IAAY,OAAO,QAAQ,KAAK,WAAW,EAAE,EAAE,OAAO;;AAExD,EAAE,IAAI,IAAI,GAAG,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AACtE,EAAE,IAAI,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC;AAC7C,EAAE,KAAK,CAAC,IAAI,GAAG,UAAU;;AAEzB,EAAE,IAAI,QAAQ,KAAK,KAAK,EAAE;AAC1B,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE;AACzB,MAAM,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC;AAC/C,KAAK,MAAM;AACX,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC;AAC7B;AACA,GAAG,MAAM;AACT,IAAI,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC;AAC3B;;AAEA,EAAE,IAAI,KAAK,CAAC,UAAU,EAAE;AACxB,IAAI,KAAK,CAAC,UAAU,CAAC,OAAO,GAAG,GAAG;AAClC,GAAG,MAAM;AACT,IAAI,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;AACnD;AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","x_google_ignoreList":[5]}
@@ -0,0 +1 @@
1
+ export declare const addCaptionControls: (wrapperElement: HTMLElement, styles: Record<string, string>, onCaptionRemove: () => void, onCaptionAdd: () => void) => void;
@@ -0,0 +1 @@
1
+ export declare const addImageAlignmentControls: (wrapperElement: HTMLElement, imageElement: HTMLImageElement, styles: Record<string, string>, onAlign: () => void) => void;
@@ -0,0 +1 @@
1
+ export declare const addImageResizeControls: (wrapperElement: HTMLElement, imageElement: HTMLImageElement, isResizing: boolean, startX: number, startWidth: number, styles: Record<string, string>, onResize: () => void) => void;
@@ -0,0 +1 @@
1
+ export declare const isMobileScreen: () => boolean;
@@ -0,0 +1 @@
1
+ export declare const removeImageControlsAndResetStyles: (clickedElement: HTMLElement, wrapperElement: HTMLElement, styles: Record<string, string>) => void;
@@ -0,0 +1,2 @@
1
+ export declare const changeFigureToImage: (wrapperElement: HTMLElement) => void;
2
+ export declare const changeImageToFigure: (wrapperElement: HTMLElement, captionElement: HTMLElement) => void;
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@pentestpad/tiptap-extension-figure",
3
+ "version": "1.0.0",
4
+ "description": "An extension for Tiptap that allows you to add and edit captions for images as well as align and resize them.",
5
+ "license": "MIT",
6
+ "main": "dist/index.cjs.js",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "scripts": {
10
+ "clean": "rm -rf dist",
11
+ "build": "npm run clean && rollup -c",
12
+ "dev": "npm run clean && rollup -c -w"
13
+ },
14
+ "devDependencies": {
15
+ "@rollup/plugin-babel": "^6.0.4",
16
+ "@rollup/plugin-commonjs": "^28.0.2",
17
+ "@rollup/plugin-url": "^8.0.2",
18
+ "@tiptap/core": "^2.11.5",
19
+ "@tiptap/extension-image": "^2.11.5",
20
+ "@tiptap/pm": "^2.11.5",
21
+ "rollup": "^4.34.6",
22
+ "rollup-plugin-auto-external": "^2.0.0",
23
+ "rollup-plugin-postcss": "^4.0.2",
24
+ "rollup-plugin-sourcemaps": "^0.6.3",
25
+ "rollup-plugin-typescript2": "^0.36.0",
26
+ "typescript": "^5.7.3"
27
+ },
28
+ "peerDependencies": {
29
+ "@tiptap/core": "^2.0.0",
30
+ "@tiptap/extension-image": "^2.0.0",
31
+ "@tiptap/pm": "^2.0.0"
32
+ }
33
+ }
@@ -0,0 +1,48 @@
1
+ // rollup.config.js
2
+
3
+ const autoExternal = require("rollup-plugin-auto-external");
4
+ const sourcemaps = require("rollup-plugin-sourcemaps");
5
+ const commonjs = require("@rollup/plugin-commonjs");
6
+ const babel = require("@rollup/plugin-babel");
7
+ const typescript = require("rollup-plugin-typescript2");
8
+ const url = require("@rollup/plugin-url");
9
+ const postcss = require("rollup-plugin-postcss");
10
+
11
+ const config = {
12
+ input: "src/index.ts",
13
+ output: [
14
+ {
15
+ file: "dist/index.cjs.js",
16
+ format: "cjs",
17
+ exports: "named",
18
+ sourcemap: true,
19
+ },
20
+ {
21
+ file: "dist/index.js",
22
+ format: "esm",
23
+ exports: "named",
24
+ sourcemap: true,
25
+ },
26
+ ],
27
+ plugins: [
28
+ autoExternal({ packagePath: "./package.json" }),
29
+ sourcemaps({ include: "node_modules/**" }),
30
+ babel({
31
+ babelHelpers: "bundled",
32
+ exclude: "node_modules/**",
33
+ }),
34
+ commonjs(),
35
+ typescript(),
36
+ url({
37
+ include: ["**/*.svg"],
38
+ limit: Infinity, // Embed all assets
39
+ }),
40
+ postcss({
41
+ inject: true,
42
+ modules: true,
43
+ minimize: false,
44
+ }),
45
+ ],
46
+ };
47
+
48
+ module.exports = config;
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#000000"><path d="M480-480Zm120 288H216q-29.7 0-50.85-21.16Q144-234.32 144-264.04v-432.24Q144-726 165.15-747T216-768h528q29.7 0 50.85 21.15Q816-725.7 816-696v288h-72v-288H216v432h384v72Zm144 72v-72h-72v-72h72v-72h72v72h72v72h-72v72h-72ZM293.29-368h111.86Q421-368 432-378.78q11-10.78 11-26.72V-443h-56.14v19H312v-112h75v19h56v-37.89q0-16.11-10.64-26.61Q421.73-592 406-592H293.01q-16.01 0-26.51 10.71-10.5 10.7-10.5 26.52v148.95Q256-390 266.72-379t26.57 11Zm261.22 0h112.55q15.94 0 26.44-10.78Q704-389.56 704-405.5V-443h-56.14v19H573v-112h75v19h56v-37.89q0-16.11-10.72-26.61T666.71-592H554.85Q539-592 528-581.29q-11 10.7-11 26.52v148.95Q517-390 527.79-379q10.78 11 26.72 11Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#000000"><path d="M312-144q-29.7 0-50.85-21.15Q240-186.3 240-216v-480h-48v-72h192v-48h192v48h192v72h-48v479.57Q720-186 698.85-165T648-144H312Zm336-552H312v480h336v-480ZM384-288h72v-336h-72v336Zm120 0h72v-336h-72v336ZM312-696v480-480Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#000000"><path d="M144-144v-72h672v72H144Zm144-150v-72h384v72H288ZM144-444v-72h672v72H144Zm144-150v-72h384v72H288ZM144-744v-72h672v72H144Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#000000"><path d="M144-144v-72h672v72H144Zm0-150v-72h480v72H144Zm0-150v-72h672v72H144Zm0-150v-72h480v72H144Zm0-150v-72h672v72H144Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#000000"><path d="M144-744v-72h672v72H144Zm192 150v-72h480v72H336ZM144-444v-72h672v72H144Zm192 150v-72h480v72H336ZM144-144v-72h672v72H144Z"/></svg>
@@ -0,0 +1,4 @@
1
+ declare module "*.svg" {
2
+ const content: string;
3
+ export default content;
4
+ }