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.
@@ -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>