ep_media_upload 0.2.3 → 0.2.5
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/LICENSE +191 -191
- package/README.md +91 -97
- package/ep.json +20 -20
- package/index.js +567 -538
- package/locales/en.json +12 -12
- package/package.json +56 -40
- package/static/css/ep_media_upload.css +102 -100
- package/static/images/upload-file-svgrepo-com.svg +6 -2
- package/static/js/clientHooks.js +284 -284
- package/templates/uploadButton.ejs +8 -8
- package/templates/uploadModal.ejs +12 -12
package/static/js/clientHooks.js
CHANGED
|
@@ -1,284 +1,284 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ep_media_upload - Client-side hooks
|
|
5
|
-
*
|
|
6
|
-
* Handles file selection, S3 upload via presigned URL, and hyperlink insertion
|
|
7
|
-
* compatible with ep_hyperlinked_text.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
console.log('[ep_media_upload] Client hooks loaded');
|
|
11
|
-
|
|
12
|
-
// Store ace context for hyperlink insertion
|
|
13
|
-
let _aceContext = null;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Modal management functions
|
|
17
|
-
*/
|
|
18
|
-
const showModal = (state = 'progress') => {
|
|
19
|
-
const modal = $('#mediaUploadModal');
|
|
20
|
-
const progressEl = $('#mediaUploadProgress');
|
|
21
|
-
const errorEl = $('#mediaUploadError');
|
|
22
|
-
|
|
23
|
-
// Hide all states
|
|
24
|
-
progressEl.hide();
|
|
25
|
-
errorEl.hide();
|
|
26
|
-
|
|
27
|
-
// Show requested state
|
|
28
|
-
if (state === 'progress') {
|
|
29
|
-
progressEl.show();
|
|
30
|
-
} else if (state === 'error') {
|
|
31
|
-
errorEl.show();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
modal.addClass('visible');
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const hideModal = () => {
|
|
38
|
-
$('#mediaUploadModal').removeClass('visible');
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const showError = (message) => {
|
|
42
|
-
const errorText = message || 'Upload failed.';
|
|
43
|
-
$('.ep-media-upload-error-text').text(errorText);
|
|
44
|
-
showModal('error');
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Validate file against configured restrictions
|
|
49
|
-
*/
|
|
50
|
-
const validateFile = (file) => {
|
|
51
|
-
const config = clientVars.ep_media_upload || {};
|
|
52
|
-
|
|
53
|
-
// Check file type
|
|
54
|
-
if (config.fileTypes && Array.isArray(config.fileTypes)) {
|
|
55
|
-
const nameParts = file.name.split('.');
|
|
56
|
-
if (nameParts.length < 2) {
|
|
57
|
-
const errorMsg = html10n.get('ep_media_upload.error.fileType') || 'File type not allowed.';
|
|
58
|
-
return { valid: false, error: `${errorMsg} File must have an extension.` };
|
|
59
|
-
}
|
|
60
|
-
const ext = nameParts.pop().toLowerCase();
|
|
61
|
-
if (!config.fileTypes.includes(ext)) {
|
|
62
|
-
const allowedTypes = config.fileTypes.join(', ');
|
|
63
|
-
const errorMsg = html10n.get('ep_media_upload.error.fileType') || 'File type not allowed.';
|
|
64
|
-
return { valid: false, error: `${errorMsg} Allowed: ${allowedTypes}` };
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Check file size
|
|
69
|
-
if (config.maxFileSize && file.size > config.maxFileSize) {
|
|
70
|
-
const maxMB = (config.maxFileSize / (1024 * 1024)).toFixed(1);
|
|
71
|
-
const errorMsg = html10n.get('ep_media_upload.error.fileSize', { maxallowed: maxMB })
|
|
72
|
-
|| `File is too large. Maximum size is ${maxMB} MB.`;
|
|
73
|
-
return { valid: false, error: errorMsg };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return { valid: true };
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Upload file to S3 using presigned URL
|
|
81
|
-
* Returns the secure download URL (relative path to our authenticated endpoint)
|
|
82
|
-
*/
|
|
83
|
-
const uploadToS3 = async (file) => {
|
|
84
|
-
// Step 1: Get presigned URL from server
|
|
85
|
-
const queryParams = $.param({ name: file.name, type: file.type });
|
|
86
|
-
const presignResponse = await $.getJSON(
|
|
87
|
-
`${encodeURIComponent(clientVars.padId)}/pluginfw/ep_media_upload/s3_presign?${queryParams}`
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
if (!presignResponse || !presignResponse.signedUrl || !presignResponse.downloadUrl) {
|
|
91
|
-
throw new Error('Invalid presign response from server');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Step 2: Upload directly to S3
|
|
95
|
-
// Must include Content-Disposition header as it's part of the presigned URL signature
|
|
96
|
-
const headers = { 'Content-Type': file.type };
|
|
97
|
-
if (presignResponse.contentDisposition) {
|
|
98
|
-
headers['Content-Disposition'] = presignResponse.contentDisposition;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const uploadResponse = await fetch(presignResponse.signedUrl, {
|
|
102
|
-
method: 'PUT',
|
|
103
|
-
headers,
|
|
104
|
-
body: file,
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
if (!uploadResponse.ok) {
|
|
108
|
-
throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Return the secure download URL (authenticated endpoint, not direct S3)
|
|
112
|
-
return presignResponse.downloadUrl;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Insert hyperlink at cursor position
|
|
117
|
-
* Compatible with ep_hyperlinked_text format
|
|
118
|
-
*/
|
|
119
|
-
const doInsertMediaLink = function(url, linkText) {
|
|
120
|
-
const editorInfo = this.editorInfo;
|
|
121
|
-
const docMan = this.documentAttributeManager;
|
|
122
|
-
const rep = editorInfo.ace_getRep();
|
|
123
|
-
|
|
124
|
-
if (!editorInfo || !rep || !rep.selStart || !docMan || !url || !linkText) {
|
|
125
|
-
console.error('[ep_media_upload] Missing context for hyperlink insertion');
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const cursorPos = rep.selStart;
|
|
130
|
-
const ZWSP = '\u200B'; // Zero-Width Space for boundary
|
|
131
|
-
|
|
132
|
-
// Insert: ZWSP + linkText + ZWSP (same pattern as ep_hyperlinked_text)
|
|
133
|
-
const textToInsert = ZWSP + linkText + ZWSP;
|
|
134
|
-
editorInfo.ace_replaceRange(cursorPos, cursorPos, textToInsert);
|
|
135
|
-
|
|
136
|
-
// Apply hyperlink attribute to the linkText portion (excluding ZWSPs)
|
|
137
|
-
const linkStart = [cursorPos[0], cursorPos[1] + ZWSP.length];
|
|
138
|
-
const linkEnd = [cursorPos[0], cursorPos[1] + ZWSP.length + linkText.length];
|
|
139
|
-
|
|
140
|
-
docMan.setAttributesOnRange(linkStart, linkEnd, [['hyperlink', url]]);
|
|
141
|
-
|
|
142
|
-
// Move cursor after the inserted content
|
|
143
|
-
const finalPos = [cursorPos[0], cursorPos[1] + textToInsert.length];
|
|
144
|
-
editorInfo.ace_performSelectionChange(finalPos, finalPos, false);
|
|
145
|
-
|
|
146
|
-
console.log('[ep_media_upload] Inserted hyperlink:', linkText, '->', url);
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Handle file selection and upload
|
|
151
|
-
*/
|
|
152
|
-
const handleFileUpload = async (file, aceContext) => {
|
|
153
|
-
// Validate file
|
|
154
|
-
const validation = validateFile(file);
|
|
155
|
-
if (!validation.valid) {
|
|
156
|
-
showError(validation.error);
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Show progress modal
|
|
161
|
-
showModal('progress');
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
// Upload to S3 and get secure download URL
|
|
165
|
-
const downloadUrl = await uploadToS3(file);
|
|
166
|
-
|
|
167
|
-
// Insert hyperlink into document (uses authenticated download endpoint)
|
|
168
|
-
aceContext.callWithAce((ace) => {
|
|
169
|
-
ace.ace_doInsertMediaLink(downloadUrl, file.name);
|
|
170
|
-
}, 'insertMediaLink', true);
|
|
171
|
-
|
|
172
|
-
// Hide modal on success (no success message needed)
|
|
173
|
-
hideModal();
|
|
174
|
-
|
|
175
|
-
} catch (err) {
|
|
176
|
-
console.error('[ep_media_upload] Upload failed:', err);
|
|
177
|
-
// Extract error message from various error formats
|
|
178
|
-
let errorMsg = 'Upload failed.';
|
|
179
|
-
if (err.responseJSON && err.responseJSON.error) {
|
|
180
|
-
// jQuery AJAX error with JSON response
|
|
181
|
-
errorMsg = err.responseJSON.error;
|
|
182
|
-
} else if (err.message) {
|
|
183
|
-
// Standard Error object
|
|
184
|
-
errorMsg = err.message;
|
|
185
|
-
} else if (typeof err === 'string') {
|
|
186
|
-
errorMsg = err;
|
|
187
|
-
}
|
|
188
|
-
showError(errorMsg);
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* aceInitialized hook
|
|
194
|
-
* Bind the hyperlink insertion function to ace context
|
|
195
|
-
*/
|
|
196
|
-
exports.aceInitialized = (hook, context) => {
|
|
197
|
-
context.editorInfo.ace_doInsertMediaLink = doInsertMediaLink.bind(context);
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* postAceInit hook
|
|
202
|
-
* Set up modal close button handler
|
|
203
|
-
*/
|
|
204
|
-
exports.postAceInit = (hook, context) => {
|
|
205
|
-
_aceContext = context.ace;
|
|
206
|
-
|
|
207
|
-
// Close button handler for error modal
|
|
208
|
-
$(document).on('click', '#mediaUploadErrorClose', () => {
|
|
209
|
-
hideModal();
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
// Click outside modal to close (only for error state)
|
|
213
|
-
$(document).on('click', '#mediaUploadModal', (e) => {
|
|
214
|
-
if (e.target.id === 'mediaUploadModal' && $('#mediaUploadError').is(':visible')) {
|
|
215
|
-
hideModal();
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* postToolbarInit hook
|
|
222
|
-
* Register the mediaUpload toolbar command
|
|
223
|
-
*/
|
|
224
|
-
exports.postToolbarInit = (hook, context) => {
|
|
225
|
-
const toolbar = context.toolbar;
|
|
226
|
-
|
|
227
|
-
toolbar.registerCommand('mediaUpload', () => {
|
|
228
|
-
// Remove any existing file input (cleanup from previous attempts)
|
|
229
|
-
$('#mediaUploadFileInput').remove();
|
|
230
|
-
|
|
231
|
-
// Create hidden file input
|
|
232
|
-
const fileInput = $('<input>')
|
|
233
|
-
.attr({
|
|
234
|
-
type: 'file',
|
|
235
|
-
id: 'mediaUploadFileInput',
|
|
236
|
-
style: 'position:absolute;left:-9999px;'
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
// Add accept attribute if file types are configured
|
|
240
|
-
if (clientVars.ep_media_upload && clientVars.ep_media_upload.fileTypes) {
|
|
241
|
-
const accept = clientVars.ep_media_upload.fileTypes
|
|
242
|
-
.map(ext => `.${ext}`)
|
|
243
|
-
.join(',');
|
|
244
|
-
fileInput.attr('accept', accept);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
$('body').append(fileInput);
|
|
248
|
-
|
|
249
|
-
// Cleanup function to remove file input
|
|
250
|
-
const cleanup = () => {
|
|
251
|
-
fileInput.off(); // Remove all event handlers
|
|
252
|
-
fileInput.remove();
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
// Handle file selection - use 'one' so it only fires once
|
|
256
|
-
fileInput.one('change', (e) => {
|
|
257
|
-
const files = e.target.files;
|
|
258
|
-
if (!files || files.length === 0) {
|
|
259
|
-
cleanup();
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const file = files[0];
|
|
264
|
-
handleFileUpload(file, context.ace);
|
|
265
|
-
cleanup();
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
// Handle cancel (user closes file picker without selecting)
|
|
269
|
-
// The blur/focus trick: when file picker closes, window regains focus
|
|
270
|
-
$(window).one('focus', () => {
|
|
271
|
-
// Small delay to allow change event to fire first if file was selected
|
|
272
|
-
setTimeout(() => {
|
|
273
|
-
if ($('#mediaUploadFileInput').length > 0) {
|
|
274
|
-
cleanup();
|
|
275
|
-
}
|
|
276
|
-
}, 300);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
// Trigger file picker
|
|
280
|
-
fileInput.trigger('click');
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
console.log('[ep_media_upload] Toolbar command registered');
|
|
284
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ep_media_upload - Client-side hooks
|
|
5
|
+
*
|
|
6
|
+
* Handles file selection, S3 upload via presigned URL, and hyperlink insertion
|
|
7
|
+
* compatible with ep_hyperlinked_text.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
console.log('[ep_media_upload] Client hooks loaded');
|
|
11
|
+
|
|
12
|
+
// Store ace context for hyperlink insertion
|
|
13
|
+
let _aceContext = null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Modal management functions
|
|
17
|
+
*/
|
|
18
|
+
const showModal = (state = 'progress') => {
|
|
19
|
+
const modal = $('#mediaUploadModal');
|
|
20
|
+
const progressEl = $('#mediaUploadProgress');
|
|
21
|
+
const errorEl = $('#mediaUploadError');
|
|
22
|
+
|
|
23
|
+
// Hide all states
|
|
24
|
+
progressEl.hide();
|
|
25
|
+
errorEl.hide();
|
|
26
|
+
|
|
27
|
+
// Show requested state
|
|
28
|
+
if (state === 'progress') {
|
|
29
|
+
progressEl.show();
|
|
30
|
+
} else if (state === 'error') {
|
|
31
|
+
errorEl.show();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
modal.addClass('visible');
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const hideModal = () => {
|
|
38
|
+
$('#mediaUploadModal').removeClass('visible');
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const showError = (message) => {
|
|
42
|
+
const errorText = message || 'Upload failed.';
|
|
43
|
+
$('.ep-media-upload-error-text').text(errorText);
|
|
44
|
+
showModal('error');
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validate file against configured restrictions
|
|
49
|
+
*/
|
|
50
|
+
const validateFile = (file) => {
|
|
51
|
+
const config = clientVars.ep_media_upload || {};
|
|
52
|
+
|
|
53
|
+
// Check file type
|
|
54
|
+
if (config.fileTypes && Array.isArray(config.fileTypes)) {
|
|
55
|
+
const nameParts = file.name.split('.');
|
|
56
|
+
if (nameParts.length < 2) {
|
|
57
|
+
const errorMsg = html10n.get('ep_media_upload.error.fileType') || 'File type not allowed.';
|
|
58
|
+
return { valid: false, error: `${errorMsg} File must have an extension.` };
|
|
59
|
+
}
|
|
60
|
+
const ext = nameParts.pop().toLowerCase();
|
|
61
|
+
if (!config.fileTypes.includes(ext)) {
|
|
62
|
+
const allowedTypes = config.fileTypes.join(', ');
|
|
63
|
+
const errorMsg = html10n.get('ep_media_upload.error.fileType') || 'File type not allowed.';
|
|
64
|
+
return { valid: false, error: `${errorMsg} Allowed: ${allowedTypes}` };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check file size
|
|
69
|
+
if (config.maxFileSize && file.size > config.maxFileSize) {
|
|
70
|
+
const maxMB = (config.maxFileSize / (1024 * 1024)).toFixed(1);
|
|
71
|
+
const errorMsg = html10n.get('ep_media_upload.error.fileSize', { maxallowed: maxMB })
|
|
72
|
+
|| `File is too large. Maximum size is ${maxMB} MB.`;
|
|
73
|
+
return { valid: false, error: errorMsg };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { valid: true };
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Upload file to S3 using presigned URL
|
|
81
|
+
* Returns the secure download URL (relative path to our authenticated endpoint)
|
|
82
|
+
*/
|
|
83
|
+
const uploadToS3 = async (file) => {
|
|
84
|
+
// Step 1: Get presigned URL from server
|
|
85
|
+
const queryParams = $.param({ name: file.name, type: file.type });
|
|
86
|
+
const presignResponse = await $.getJSON(
|
|
87
|
+
`${encodeURIComponent(clientVars.padId)}/pluginfw/ep_media_upload/s3_presign?${queryParams}`
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (!presignResponse || !presignResponse.signedUrl || !presignResponse.downloadUrl) {
|
|
91
|
+
throw new Error('Invalid presign response from server');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Step 2: Upload directly to S3
|
|
95
|
+
// Must include Content-Disposition header as it's part of the presigned URL signature
|
|
96
|
+
const headers = { 'Content-Type': file.type };
|
|
97
|
+
if (presignResponse.contentDisposition) {
|
|
98
|
+
headers['Content-Disposition'] = presignResponse.contentDisposition;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const uploadResponse = await fetch(presignResponse.signedUrl, {
|
|
102
|
+
method: 'PUT',
|
|
103
|
+
headers,
|
|
104
|
+
body: file,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (!uploadResponse.ok) {
|
|
108
|
+
throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Return the secure download URL (authenticated endpoint, not direct S3)
|
|
112
|
+
return presignResponse.downloadUrl;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Insert hyperlink at cursor position
|
|
117
|
+
* Compatible with ep_hyperlinked_text format
|
|
118
|
+
*/
|
|
119
|
+
const doInsertMediaLink = function(url, linkText) {
|
|
120
|
+
const editorInfo = this.editorInfo;
|
|
121
|
+
const docMan = this.documentAttributeManager;
|
|
122
|
+
const rep = editorInfo.ace_getRep();
|
|
123
|
+
|
|
124
|
+
if (!editorInfo || !rep || !rep.selStart || !docMan || !url || !linkText) {
|
|
125
|
+
console.error('[ep_media_upload] Missing context for hyperlink insertion');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const cursorPos = rep.selStart;
|
|
130
|
+
const ZWSP = '\u200B'; // Zero-Width Space for boundary
|
|
131
|
+
|
|
132
|
+
// Insert: ZWSP + linkText + ZWSP (same pattern as ep_hyperlinked_text)
|
|
133
|
+
const textToInsert = ZWSP + linkText + ZWSP;
|
|
134
|
+
editorInfo.ace_replaceRange(cursorPos, cursorPos, textToInsert);
|
|
135
|
+
|
|
136
|
+
// Apply hyperlink attribute to the linkText portion (excluding ZWSPs)
|
|
137
|
+
const linkStart = [cursorPos[0], cursorPos[1] + ZWSP.length];
|
|
138
|
+
const linkEnd = [cursorPos[0], cursorPos[1] + ZWSP.length + linkText.length];
|
|
139
|
+
|
|
140
|
+
docMan.setAttributesOnRange(linkStart, linkEnd, [['hyperlink', url]]);
|
|
141
|
+
|
|
142
|
+
// Move cursor after the inserted content
|
|
143
|
+
const finalPos = [cursorPos[0], cursorPos[1] + textToInsert.length];
|
|
144
|
+
editorInfo.ace_performSelectionChange(finalPos, finalPos, false);
|
|
145
|
+
|
|
146
|
+
console.log('[ep_media_upload] Inserted hyperlink:', linkText, '->', url);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Handle file selection and upload
|
|
151
|
+
*/
|
|
152
|
+
const handleFileUpload = async (file, aceContext) => {
|
|
153
|
+
// Validate file
|
|
154
|
+
const validation = validateFile(file);
|
|
155
|
+
if (!validation.valid) {
|
|
156
|
+
showError(validation.error);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Show progress modal
|
|
161
|
+
showModal('progress');
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
// Upload to S3 and get secure download URL
|
|
165
|
+
const downloadUrl = await uploadToS3(file);
|
|
166
|
+
|
|
167
|
+
// Insert hyperlink into document (uses authenticated download endpoint)
|
|
168
|
+
aceContext.callWithAce((ace) => {
|
|
169
|
+
ace.ace_doInsertMediaLink(downloadUrl, file.name);
|
|
170
|
+
}, 'insertMediaLink', true);
|
|
171
|
+
|
|
172
|
+
// Hide modal on success (no success message needed)
|
|
173
|
+
hideModal();
|
|
174
|
+
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.error('[ep_media_upload] Upload failed:', err);
|
|
177
|
+
// Extract error message from various error formats
|
|
178
|
+
let errorMsg = 'Upload failed.';
|
|
179
|
+
if (err.responseJSON && err.responseJSON.error) {
|
|
180
|
+
// jQuery AJAX error with JSON response
|
|
181
|
+
errorMsg = err.responseJSON.error;
|
|
182
|
+
} else if (err.message) {
|
|
183
|
+
// Standard Error object
|
|
184
|
+
errorMsg = err.message;
|
|
185
|
+
} else if (typeof err === 'string') {
|
|
186
|
+
errorMsg = err;
|
|
187
|
+
}
|
|
188
|
+
showError(errorMsg);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* aceInitialized hook
|
|
194
|
+
* Bind the hyperlink insertion function to ace context
|
|
195
|
+
*/
|
|
196
|
+
exports.aceInitialized = (hook, context) => {
|
|
197
|
+
context.editorInfo.ace_doInsertMediaLink = doInsertMediaLink.bind(context);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* postAceInit hook
|
|
202
|
+
* Set up modal close button handler
|
|
203
|
+
*/
|
|
204
|
+
exports.postAceInit = (hook, context) => {
|
|
205
|
+
_aceContext = context.ace;
|
|
206
|
+
|
|
207
|
+
// Close button handler for error modal
|
|
208
|
+
$(document).on('click', '#mediaUploadErrorClose', () => {
|
|
209
|
+
hideModal();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Click outside modal to close (only for error state)
|
|
213
|
+
$(document).on('click', '#mediaUploadModal', (e) => {
|
|
214
|
+
if (e.target.id === 'mediaUploadModal' && $('#mediaUploadError').is(':visible')) {
|
|
215
|
+
hideModal();
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* postToolbarInit hook
|
|
222
|
+
* Register the mediaUpload toolbar command
|
|
223
|
+
*/
|
|
224
|
+
exports.postToolbarInit = (hook, context) => {
|
|
225
|
+
const toolbar = context.toolbar;
|
|
226
|
+
|
|
227
|
+
toolbar.registerCommand('mediaUpload', () => {
|
|
228
|
+
// Remove any existing file input (cleanup from previous attempts)
|
|
229
|
+
$('#mediaUploadFileInput').remove();
|
|
230
|
+
|
|
231
|
+
// Create hidden file input
|
|
232
|
+
const fileInput = $('<input>')
|
|
233
|
+
.attr({
|
|
234
|
+
type: 'file',
|
|
235
|
+
id: 'mediaUploadFileInput',
|
|
236
|
+
style: 'position:absolute;left:-9999px;'
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Add accept attribute if file types are configured
|
|
240
|
+
if (clientVars.ep_media_upload && clientVars.ep_media_upload.fileTypes) {
|
|
241
|
+
const accept = clientVars.ep_media_upload.fileTypes
|
|
242
|
+
.map(ext => `.${ext}`)
|
|
243
|
+
.join(',');
|
|
244
|
+
fileInput.attr('accept', accept);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
$('body').append(fileInput);
|
|
248
|
+
|
|
249
|
+
// Cleanup function to remove file input
|
|
250
|
+
const cleanup = () => {
|
|
251
|
+
fileInput.off(); // Remove all event handlers
|
|
252
|
+
fileInput.remove();
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// Handle file selection - use 'one' so it only fires once
|
|
256
|
+
fileInput.one('change', (e) => {
|
|
257
|
+
const files = e.target.files;
|
|
258
|
+
if (!files || files.length === 0) {
|
|
259
|
+
cleanup();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const file = files[0];
|
|
264
|
+
handleFileUpload(file, context.ace);
|
|
265
|
+
cleanup();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Handle cancel (user closes file picker without selecting)
|
|
269
|
+
// The blur/focus trick: when file picker closes, window regains focus
|
|
270
|
+
$(window).one('focus', () => {
|
|
271
|
+
// Small delay to allow change event to fire first if file was selected
|
|
272
|
+
setTimeout(() => {
|
|
273
|
+
if ($('#mediaUploadFileInput').length > 0) {
|
|
274
|
+
cleanup();
|
|
275
|
+
}
|
|
276
|
+
}, 300);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Trigger file picker
|
|
280
|
+
fileInput.trigger('click');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
console.log('[ep_media_upload] Toolbar command registered');
|
|
284
|
+
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
<li class="separator acl-write"></li>
|
|
2
|
-
|
|
3
|
-
<li data-type="button" data-key="mediaUpload" class="acl-write" data-l10n-id="ep_media_upload.toolbar.upload.title">
|
|
4
|
-
<a class="grouped-left ep_media_upload" data-l10n-id="ep_media_upload.toolbar.upload.title" title="Upload File" aria-label="Upload File">
|
|
5
|
-
<button class="buttonicon ep_media_upload media_upload buttonicon-attachment" aria-label="Upload File"></button>
|
|
6
|
-
</a>
|
|
7
|
-
</li>
|
|
8
|
-
|
|
1
|
+
<li class="separator acl-write"></li>
|
|
2
|
+
|
|
3
|
+
<li data-type="button" data-key="mediaUpload" class="acl-write" data-l10n-id="ep_media_upload.toolbar.upload.title">
|
|
4
|
+
<a class="grouped-left ep_media_upload" data-l10n-id="ep_media_upload.toolbar.upload.title" title="Upload File" aria-label="Upload File">
|
|
5
|
+
<button class="buttonicon ep_media_upload media_upload buttonicon-attachment" aria-label="Upload File"></button>
|
|
6
|
+
</a>
|
|
7
|
+
</li>
|
|
8
|
+
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
<div id="mediaUploadModal" class="ep-media-upload-modal">
|
|
2
|
-
<div class="ep-media-upload-modal-content">
|
|
3
|
-
<div id="mediaUploadProgress" class="ep-media-upload-state">
|
|
4
|
-
<div class="ep-media-upload-spinner"></div>
|
|
5
|
-
<p class="ep-media-upload-message" data-l10n-id="ep_media_upload.status.uploading">Uploading...</p>
|
|
6
|
-
</div>
|
|
7
|
-
<div id="mediaUploadError" class="ep-media-upload-state" style="display: none;">
|
|
8
|
-
<p class="ep-media-upload-message ep-media-upload-error-text">Upload failed.</p>
|
|
9
|
-
<button id="mediaUploadErrorClose" class="ep-media-upload-btn" data-l10n-id="ep_media_upload.button.close">Close</button>
|
|
10
|
-
</div>
|
|
11
|
-
</div>
|
|
12
|
-
</div>
|
|
1
|
+
<div id="mediaUploadModal" class="ep-media-upload-modal">
|
|
2
|
+
<div class="ep-media-upload-modal-content">
|
|
3
|
+
<div id="mediaUploadProgress" class="ep-media-upload-state">
|
|
4
|
+
<div class="ep-media-upload-spinner"></div>
|
|
5
|
+
<p class="ep-media-upload-message" data-l10n-id="ep_media_upload.status.uploading">Uploading...</p>
|
|
6
|
+
</div>
|
|
7
|
+
<div id="mediaUploadError" class="ep-media-upload-state" style="display: none;">
|
|
8
|
+
<p class="ep-media-upload-message ep-media-upload-error-text">Upload failed.</p>
|
|
9
|
+
<button id="mediaUploadErrorClose" class="ep-media-upload-btn" data-l10n-id="ep_media_upload.button.close">Close</button>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|