@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.
- package/README.md +39 -0
- package/dist/component/tip-tap-image-resize-with-caption.d.ts +5 -0
- package/dist/index.cjs.js +452 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +448 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/add-caption-controls.util.d.ts +1 -0
- package/dist/utils/add-image-alignment-controls.util.d.ts +1 -0
- package/dist/utils/add-image-resize-controls.util.d.ts +1 -0
- package/dist/utils/is-mobile-screen.util.d.ts +1 -0
- package/dist/utils/remove-image-controls.util.d.ts +1 -0
- package/dist/utils/replace-element.util.d.ts +2 -0
- package/package.json +33 -0
- package/rollup.config.js +48 -0
- package/src/assets/icons/closed-caption-add.svg +1 -0
- package/src/assets/icons/delete.svg +1 -0
- package/src/assets/icons/format-align-center.svg +1 -0
- package/src/assets/icons/format-align-left.svg +1 -0
- package/src/assets/icons/format-align-right.svg +1 -0
- package/src/assets/icons/svg.d.ts +4 -0
- package/src/assets/styles/styles.css +85 -0
- package/src/assets/styles/styles.d.ts +4 -0
- package/src/component/tip-tap-image-resize-with-caption.ts +300 -0
- package/src/index.ts +3 -0
- package/src/utils/add-caption-controls.util.ts +51 -0
- package/src/utils/add-image-alignment-controls.util.ts +62 -0
- package/src/utils/add-image-resize-controls.util.ts +104 -0
- package/src/utils/is-mobile-screen.util.ts +1 -0
- package/src/utils/remove-image-controls.util.ts +22 -0
- package/src/utils/replace-element.util.ts +44 -0
- 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;
|
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
|
+
}
|
package/rollup.config.js
ADDED
|
@@ -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>
|