ep_images_extended 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/CONTRIBUTING.md +135 -0
- package/LICENSE.md +14 -0
- package/README.md +83 -0
- package/editbar.js +10 -0
- package/ep.json +34 -0
- package/exportHTML.js +81 -0
- package/index.js +183 -0
- package/locales/en.json +6 -0
- package/package.json +30 -0
- package/settings.js +11 -0
- package/static/css/ace.css +346 -0
- package/static/css/ep_images_extended.css +15 -0
- package/static/js/clientHooks.js +1848 -0
- package/static/js/contentCollection.js +96 -0
- package/static/js/toolbar.js +177 -0
- package/templates/editbarButton.ejs +7 -0
- package/templates/imageFormatMenu.ejs +29 -0
- package/templates/modal.ejs +13 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// This hook is called **before** the text of a line/segment is processed by the Changeset library.
|
|
4
|
+
const collectContentPre = (hook, context) => {
|
|
5
|
+
const classes = context.cls ? context.cls.split(' ') : [];
|
|
6
|
+
let escapedSrc = null;
|
|
7
|
+
let widthValue = null;
|
|
8
|
+
let heightValue = null;
|
|
9
|
+
let aspectRatioValue = null;
|
|
10
|
+
let floatValue = null;
|
|
11
|
+
let imageIdValue = null;
|
|
12
|
+
|
|
13
|
+
for (const cls of classes) {
|
|
14
|
+
if (cls.startsWith('image:')) {
|
|
15
|
+
escapedSrc = cls.substring(6);
|
|
16
|
+
} else if (cls.startsWith('image-width:')) {
|
|
17
|
+
const potentialWidth = cls.substring(12);
|
|
18
|
+
if (potentialWidth && (potentialWidth === 'auto' || /[0-9]+(%|px|em|rem|vw|vh)?$/.test(potentialWidth) || /^[0-9.]+$/.test(potentialWidth))) {
|
|
19
|
+
widthValue = potentialWidth;
|
|
20
|
+
}
|
|
21
|
+
} else if (cls.startsWith('image-height:')) {
|
|
22
|
+
const potentialHeight = cls.substring(13);
|
|
23
|
+
if (potentialHeight && (potentialHeight === 'auto' || /[0-9]+(%|px|em|rem|vw|vh)?$/.test(potentialHeight) || /^[0-9.]+$/.test(potentialHeight))) {
|
|
24
|
+
heightValue = potentialHeight;
|
|
25
|
+
}
|
|
26
|
+
} else if (cls.startsWith('imageCssAspectRatio:')) {
|
|
27
|
+
const potentialAspectRatio = cls.substring(20);
|
|
28
|
+
if (!isNaN(parseFloat(potentialAspectRatio))) {
|
|
29
|
+
aspectRatioValue = potentialAspectRatio;
|
|
30
|
+
}
|
|
31
|
+
} else if (cls.startsWith('image-float:')) {
|
|
32
|
+
const potentialFloat = cls.substring(12);
|
|
33
|
+
if (potentialFloat && ['none', 'left', 'right', 'inline'].includes(potentialFloat)) {
|
|
34
|
+
floatValue = potentialFloat;
|
|
35
|
+
}
|
|
36
|
+
} else if (cls.startsWith('image-id-')) {
|
|
37
|
+
const potentialId = cls.substring(9);
|
|
38
|
+
if (potentialId && potentialId.length > 10) {
|
|
39
|
+
imageIdValue = potentialId;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (escapedSrc) {
|
|
45
|
+
try {
|
|
46
|
+
context.cc.doAttrib(context.state, `image::${escapedSrc}`);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.error('[ep_images_extended collectContentPre] Error applying image attribute:', e);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (widthValue) {
|
|
52
|
+
try {
|
|
53
|
+
context.cc.doAttrib(context.state, `image-width::${widthValue}`);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.error('[ep_images_extended collectContentPre] Error applying image-width attribute:', e);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (heightValue) {
|
|
59
|
+
try {
|
|
60
|
+
context.cc.doAttrib(context.state, `image-height::${heightValue}`);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.error('[ep_images_extended collectContentPre] Error applying image-height attribute:', e);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (aspectRatioValue) {
|
|
66
|
+
try {
|
|
67
|
+
context.cc.doAttrib(context.state, `imageCssAspectRatio::${aspectRatioValue}`);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.error('[ep_images_extended collectContentPre] Error applying imageCssAspectRatio attribute:', e);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (floatValue) {
|
|
73
|
+
try {
|
|
74
|
+
context.cc.doAttrib(context.state, `image-float::${floatValue}`);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
console.error('[ep_images_extended collectContentPre] Error applying image-float attribute:', e);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (imageIdValue) {
|
|
80
|
+
try {
|
|
81
|
+
context.cc.doAttrib(context.state, `image-id::${imageIdValue}`);
|
|
82
|
+
} catch (e) {
|
|
83
|
+
console.error('[ep_images_extended collectContentPre] Error applying image-id attribute:', e);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// This hook is called **after** the text of a line/segment is processed.
|
|
89
|
+
// We don't need special post-processing for this attribute approach.
|
|
90
|
+
const collectContentPost = (hook, context) => {};
|
|
91
|
+
|
|
92
|
+
// Remove collectContentImage as it's not suitable for non-<img> elements
|
|
93
|
+
// const collectContentImage = ... (Removed)
|
|
94
|
+
|
|
95
|
+
exports.collectContentPre = collectContentPre;
|
|
96
|
+
exports.collectContentPost = collectContentPost;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const _isValid = (file) => {
|
|
4
|
+
const mimedb = clientVars.ep_images_extended.mimeTypes;
|
|
5
|
+
const mimeType = mimedb[file.type];
|
|
6
|
+
let validMime = null;
|
|
7
|
+
const errorTitle = html10n.get('ep_images_extended.error.title');
|
|
8
|
+
|
|
9
|
+
if (clientVars.ep_images_extended && clientVars.ep_images_extended.fileTypes) {
|
|
10
|
+
validMime = false;
|
|
11
|
+
if (mimeType && mimeType.extensions) {
|
|
12
|
+
for (const fileType of clientVars.ep_images_extended.fileTypes) {
|
|
13
|
+
const exists = mimeType.extensions.indexOf(fileType);
|
|
14
|
+
if (exists > -1) {
|
|
15
|
+
validMime = true;
|
|
16
|
+
break; // Found a valid type
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (validMime === false) {
|
|
21
|
+
const errorMessage = html10n.get('ep_images_extended.error.fileType');
|
|
22
|
+
$.gritter.add({ title: errorTitle, text: errorMessage, sticky: true, class_name: 'error' });
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (clientVars.ep_images_extended && file.size > clientVars.ep_images_extended.maxFileSize) {
|
|
28
|
+
const allowedSize = (clientVars.ep_images_extended.maxFileSize / 1000000);
|
|
29
|
+
const errorText = html10n.get('ep_images_extended.error.fileSize', { maxallowed: allowedSize });
|
|
30
|
+
$.gritter.add({ title: errorTitle, text: errorText, sticky: true, class_name: 'error' });
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Helper function to insert the image attribute with ZWSP boundaries
|
|
37
|
+
const insertInlineImage = (ace, imageData) => {
|
|
38
|
+
const ZWSP = '\u200b'; // Zero-Width Space
|
|
39
|
+
const placeholder = '\u00a0'; // Use NBSP as placeholder
|
|
40
|
+
const textToInsert = ZWSP + placeholder + ZWSP;
|
|
41
|
+
|
|
42
|
+
const escapedSrc = escape(imageData.src);
|
|
43
|
+
const attrKey = 'image'; // Standard attribute key
|
|
44
|
+
const attrValue = escapedSrc; // Escaped src is the value
|
|
45
|
+
|
|
46
|
+
// Get current selection to replace it (or insert if no selection)
|
|
47
|
+
const rep = ace.ace_getRep();
|
|
48
|
+
const start = rep.selStart;
|
|
49
|
+
const end = rep.selEnd;
|
|
50
|
+
|
|
51
|
+
// Add the placeholder characters, replacing selection if any
|
|
52
|
+
ace.ace_replaceRange(start, end, textToInsert);
|
|
53
|
+
|
|
54
|
+
// Define the range for the attribute: ONLY the middle character (the placeholder NBSP)
|
|
55
|
+
const attrStart = [start[0], start[1] + ZWSP.length]; // Start after the first ZWSP
|
|
56
|
+
const attrEnd = [start[0], start[1] + ZWSP.length + placeholder.length]; // End after the placeholder NBSP
|
|
57
|
+
|
|
58
|
+
// Apply the image attribute to the placeholder character
|
|
59
|
+
ace.ace_performDocumentApplyAttributesToRange(attrStart, attrEnd, [[attrKey, attrValue]]);
|
|
60
|
+
|
|
61
|
+
// Move cursor after all inserted characters
|
|
62
|
+
const finalEnd = [start[0], start[1] + textToInsert.length];
|
|
63
|
+
ace.ace_performSelectionChange(finalEnd, finalEnd, false);
|
|
64
|
+
ace.ace_focus();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
exports.postToolbarInit = (hook, context) => {
|
|
69
|
+
const toolbar = context.toolbar;
|
|
70
|
+
toolbar.registerCommand('imageUpload', () => {
|
|
71
|
+
$(document).find('body').find('#imageInput').remove();
|
|
72
|
+
const fileInputHtml = `<input
|
|
73
|
+
style="width:1px;height:1px;z-index:-10000;"
|
|
74
|
+
id="imageInput" type="file" />`;
|
|
75
|
+
$(document).find('body').append(fileInputHtml);
|
|
76
|
+
|
|
77
|
+
$(document).find('body').find('#imageInput').on('change', (e) => {
|
|
78
|
+
const files = e.target.files;
|
|
79
|
+
if (!files.length) {
|
|
80
|
+
// No specific user message needed here, browser handles it or no file selected is not an error
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const file = files[0];
|
|
84
|
+
|
|
85
|
+
if (!_isValid(file)) {
|
|
86
|
+
return; // Validation errors are handled by _isValid
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (clientVars.ep_images_extended.storageType === 'base64') {
|
|
90
|
+
$('#imageUploadModalLoader').removeClass('popup-show'); // Ensure loader is hidden initially
|
|
91
|
+
const reader = new FileReader();
|
|
92
|
+
reader.readAsDataURL(file);
|
|
93
|
+
reader.onload = () => {
|
|
94
|
+
const data = reader.result;
|
|
95
|
+
const img = new Image();
|
|
96
|
+
img.onload = () => {
|
|
97
|
+
const widthPx = `${img.naturalWidth}px`;
|
|
98
|
+
const heightPx = `${img.naturalHeight}px`;
|
|
99
|
+
context.ace.callWithAce((ace) => {
|
|
100
|
+
ace.ace_doInsertImage(data, widthPx, heightPx);
|
|
101
|
+
}, 'imgBase64', true);
|
|
102
|
+
};
|
|
103
|
+
img.onerror = () => {
|
|
104
|
+
console.error('[ep_images_extended toolbar] Failed to load Base64 image data to get dimensions. Inserting without dimensions.');
|
|
105
|
+
context.ace.callWithAce((ace) => {
|
|
106
|
+
ace.ace_doInsertImage(data);
|
|
107
|
+
}, 'imgBase64Error', true);
|
|
108
|
+
};
|
|
109
|
+
img.src = data;
|
|
110
|
+
};
|
|
111
|
+
reader.onerror = (error_evt) => { // Added error handling for FileReader
|
|
112
|
+
console.error('[ep_images_extended toolbar] FileReader error:', error_evt);
|
|
113
|
+
const errorTitle = html10n.get('ep_images_extended.error.title');
|
|
114
|
+
const errorMessage = html10n.get('ep_images_extended.error.fileRead'); // Generic file read error
|
|
115
|
+
$.gritter.add({ title: errorTitle, text: errorMessage, sticky: true, class_name: 'error' });
|
|
116
|
+
};
|
|
117
|
+
} else if (clientVars.ep_images_extended.storageType === 's3_presigned') {
|
|
118
|
+
// -------- Direct browser -> S3 upload via presigned URL --------
|
|
119
|
+
const queryParams = $.param({ name: file.name, type: file.type });
|
|
120
|
+
$('#imageUploadModalLoader').addClass('popup-show');
|
|
121
|
+
|
|
122
|
+
$.getJSON(`${clientVars.padId}/pluginfw/ep_images_extended/s3_presign?${queryParams}`)
|
|
123
|
+
.then((presignData) => {
|
|
124
|
+
if (!presignData || !presignData.signedUrl || !presignData.publicUrl) {
|
|
125
|
+
throw new Error('Invalid presign response');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Upload the file directly to S3
|
|
129
|
+
return fetch(presignData.signedUrl, {
|
|
130
|
+
method: 'PUT',
|
|
131
|
+
headers: { 'Content-Type': file.type },
|
|
132
|
+
body: file,
|
|
133
|
+
}).then((response) => {
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
throw new Error(`S3 upload failed with status ${response.status}`);
|
|
136
|
+
}
|
|
137
|
+
return presignData.publicUrl;
|
|
138
|
+
});
|
|
139
|
+
})
|
|
140
|
+
.then((publicUrl) => {
|
|
141
|
+
// Remove loader
|
|
142
|
+
$('#imageUploadModalLoader').removeClass('popup-show');
|
|
143
|
+
|
|
144
|
+
// Obtain intrinsic dimensions by loading image
|
|
145
|
+
const img = new Image();
|
|
146
|
+
img.onload = () => {
|
|
147
|
+
const widthPx = `${img.naturalWidth}px`;
|
|
148
|
+
const heightPx = `${img.naturalHeight}px`;
|
|
149
|
+
context.ace.callWithAce((ace) => {
|
|
150
|
+
ace.ace_doInsertImage(publicUrl, widthPx, heightPx);
|
|
151
|
+
}, 'imgUploadS3', true);
|
|
152
|
+
};
|
|
153
|
+
img.onerror = () => {
|
|
154
|
+
console.warn('[ep_images_extended toolbar] Could not load uploaded S3 image to measure size. Inserting without dimensions.');
|
|
155
|
+
context.ace.callWithAce((ace) => {
|
|
156
|
+
ace.ace_doInsertImage(publicUrl);
|
|
157
|
+
}, 'imgUploadS3Error', true);
|
|
158
|
+
};
|
|
159
|
+
img.src = publicUrl;
|
|
160
|
+
})
|
|
161
|
+
.catch((err) => {
|
|
162
|
+
console.error('[ep_images_extended toolbar] s3_presigned upload failed', err);
|
|
163
|
+
$('#imageUploadModalLoader').removeClass('popup-show');
|
|
164
|
+
const errorTitle = html10n.get('ep_images_extended.error.title');
|
|
165
|
+
$.gritter.add({ title: errorTitle, text: err.message, sticky: true, class_name: 'error' });
|
|
166
|
+
});
|
|
167
|
+
} else {
|
|
168
|
+
// Unsupported storage type – show error and abort
|
|
169
|
+
$('#imageUploadModalLoader').removeClass('popup-show');
|
|
170
|
+
const errorTitle = html10n.get('ep_images_extended.error.title');
|
|
171
|
+
const errorText = `Unsupported storageType: ${clientVars.ep_images_extended.storageType}. Only "base64" and "s3_presigned" are supported.`;
|
|
172
|
+
$.gritter.add({ title: errorTitle, text: errorText, sticky: true, class_name: 'error' });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
$(document).find('body').find('#imageInput').trigger('click');
|
|
176
|
+
});
|
|
177
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<li class="separator acl-write"></li>
|
|
2
|
+
|
|
3
|
+
<li data-type="button" data-key="imageUpload" data-l10n-id="ep_images_extended.toolbar.image_upload.title">
|
|
4
|
+
<a class="grouped-left ep_images_extended" data-align="0" data-l10n-id="ep_images_extended.toolbar.image_upload.title" title="Upload Image" aria-label="Upload Image">
|
|
5
|
+
<button class="buttonicon ep_images_extended image_upload buttonicon-picture" data-align="0" aria-label="Upload Image"></button>
|
|
6
|
+
</a>
|
|
7
|
+
</li>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<div id="imageFormatMenu" class="image-format-menu">
|
|
2
|
+
<div class="image-format-menu-section">
|
|
3
|
+
<div class="image-format-menu-buttons">
|
|
4
|
+
<button class="image-format-button active" data-wrap="inline" title="In line with text">
|
|
5
|
+
<i class="bi bi-justify"></i>
|
|
6
|
+
</button>
|
|
7
|
+
<button class="image-format-button" data-wrap="left" title="Wrap text - Image on left">
|
|
8
|
+
<i class="bi bi-text-indent-left"></i>
|
|
9
|
+
</button>
|
|
10
|
+
<button class="image-format-button" data-wrap="right" title="Wrap text - Image on right">
|
|
11
|
+
<i class="bi bi-text-indent-right"></i>
|
|
12
|
+
</button>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="image-format-menu-divider"></div>
|
|
16
|
+
<div class="image-format-menu-section">
|
|
17
|
+
<div class="image-format-menu-buttons">
|
|
18
|
+
<button class="image-format-button" data-action="copy" title="Copy image">
|
|
19
|
+
<i class="bi bi-copy"></i>
|
|
20
|
+
</button>
|
|
21
|
+
<button class="image-format-button" data-action="cut" title="Cut image">
|
|
22
|
+
<i class="bi bi-scissors"></i>
|
|
23
|
+
</button>
|
|
24
|
+
<button class="image-format-button image-delete-button" data-action="delete" title="Delete image">
|
|
25
|
+
<i class="bi bi-trash3"></i>
|
|
26
|
+
</button>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<div id="imageUploadModalError" class="popup">
|
|
2
|
+
<div class="popup-content">
|
|
3
|
+
<h2 class="error"></h2>
|
|
4
|
+
<br>
|
|
5
|
+
<button id="closeErrorModalButton">Close</button>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div id="imageUploadModalLoader" class="popup">
|
|
10
|
+
<div class="popup-content">
|
|
11
|
+
<p class="loadingAnimation"></p>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|