Skip to content
110 changes: 65 additions & 45 deletions public/app/editor/tinymce_5_settings.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
const ASSET_URL_MEDIA_SELECTOR =
'img[src^="asset://"], audio[src^="asset://"], video[src^="asset://"], iframe[src^="asset://"]';
const PLACEHOLDER_IMAGE_DATA_URL =
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';

var $exeTinyMCE = {
// imagetools is disabled because it generates base64 images
// colorpicker contextmenu textcolor . Añadidos al core, no hace falta añadir en plugins?
Expand Down Expand Up @@ -333,7 +338,7 @@ var $exeTinyMCE = {
// result = { assetUrl, blobUrl, asset }
const field = document.getElementById(field_name);
if (field) {
field.value = result.blobUrl;
field.value = result.assetUrl || result.blobUrl || '';
// Trigger change event for TinyMCE to pick up
field.dispatchEvent(new Event('change'));
}
Expand Down Expand Up @@ -383,26 +388,12 @@ var $exeTinyMCE = {
return;
}

// Use blob URL directly - it's already in AssetManager cache
// When images_upload_handler is triggered by TinyMCE (automatic_uploads: true),
// it will find this blob URL in reverseBlobCache and return immediately
// without re-processing. Later, convertBlobUrlsToAssetUrls() will convert
// blob:// to asset:// for persistence.
const assetManager = window.eXeLearning?.app?.project?._yjsBridge?.assetManager;

// Ensure blob URL is in cache (it should be, but verify)
if (assetManager && result.blobUrl && result.asset?.id) {
if (!assetManager.reverseBlobCache.has(result.blobUrl)) {
assetManager.reverseBlobCache.set(result.blobUrl, result.asset.id);
assetManager.blobURLCache.set(result.asset.id, result.blobUrl);
}
}

cb(result.blobUrl, {
// Keep asset:// in the editor model; rendering resolves it later.
cb(result.assetUrl || result.blobUrl || '', {
title: result.asset.filename || '',
text: result.asset.filename || '',
alt: '',
'data-asset-id': result.asset.id // CRITICAL: Used by convertBlobURLsToAssetRefs
'data-asset-id': result.asset.id
});
}
});
Expand All @@ -423,8 +414,11 @@ var $exeTinyMCE = {
if (assetManager && blobUri && blobUri.startsWith('blob:')) {
// Check if this blob URL is in our cache (meaning it's from AssetManager)
if (assetManager.reverseBlobCache.has(blobUri)) {
// Already an asset, no upload needed - just return the blob URL
success(blobUri);
const assetId = assetManager.reverseBlobCache.get(blobUri);
const asset = assetManager.getAssetById?.(assetId) || assetManager.getAssetMetadata?.(assetId);
const assetUrl = assetManager.getAssetUrl?.(assetId, asset?.filename || asset?.name) || `asset://${assetId}`;
// Return the canonical asset:// URL so users never see the temporary blob URL.
success(assetUrl, { 'data-asset-id': assetId });
return;
}
}
Expand All @@ -439,21 +433,8 @@ var $exeTinyMCE = {

// Extract UUID from asset:// URL (insertImage returns "asset://uuid/filename")
const assetId = assetManager.extractAssetId(assetUrl);

// Get or create blob URL for the asset (using synced method to ensure reverseBlobCache consistency)
let newBlobUrl = assetManager.getBlobURLSynced?.(assetId) ?? assetManager.blobURLCache.get(assetId);
if (!newBlobUrl) {
// Use the original blob directly (works for both new and deduplicated assets)
// since we already have it in memory
newBlobUrl = URL.createObjectURL(blob);
assetManager.blobURLCache.set(assetId, newBlobUrl);
assetManager.reverseBlobCache.set(newBlobUrl, assetId);
} else if (!assetManager.reverseBlobCache.has(newBlobUrl)) {
// CRITICAL: Ensure reverseBlobCache is synced - this is required for convertBlobUrlsToAssetUrls
assetManager.reverseBlobCache.set(newBlobUrl, assetId);
}
// CRITICAL: Pass data-asset-id so convertBlobURLsToAssetRefs can convert even if blob URL changes
success(newBlobUrl, { 'data-asset-id': assetId });
// Persist the canonical asset:// URL and let the editor resolve it for display.
success(assetUrl, { 'data-asset-id': assetId });
} catch (err) {
console.error('[TinyMCE] Failed to store in AssetManager:', err);
failure(_('Error storing image'));
Expand Down Expand Up @@ -570,6 +551,12 @@ var $exeTinyMCE = {
this.buttons3,
],
setup: function (ed) {
ed.on('BeforeSetContent', function(e) {
if (typeof e.content !== 'string') return;

e.content = $exeTinyMCE.prepareContentForEditorLoad(e.content);
});

// Register SetContent handler BEFORE content is loaded
// This is critical for resolving asset:// URLs in the initial content
ed.on('SetContent', function(e) {
Expand All @@ -596,6 +583,13 @@ var $exeTinyMCE = {

$exeTinyMCE.resolveAssetUrlsInEditor(ed);
});

ed.on('GetContent', function(e) {
const assetManager = window.eXeLearning?.app?.project?._yjsBridge?.assetManager;
if (assetManager?.prepareHtmlForSync && typeof e.content === 'string') {
e.content = assetManager.prepareHtmlForSync(e.content);
}
});
},
init_instance_callback: function (ed) {
if (mode == 'multiple') {
Expand All @@ -611,7 +605,7 @@ var $exeTinyMCE = {
// Note: SetContent handler is now registered in setup() callback
// to catch the initial content load before init_instance_callback runs

// Also observe DOM changes for dynamically inserted media (e.g., audio recorder, PDF embed)
// Also observe DOM changes for dynamically inserted media (e.g., images, audio recorder, PDF embed)
const editorBody = ed.getBody();
if (editorBody) {
const observer = new MutationObserver(function(mutations) {
Expand All @@ -620,8 +614,8 @@ var $exeTinyMCE = {
if (mutation.type === 'childList') {
for (const node of mutation.addedNodes) {
if (node.nodeType === 1) {
const hasAssetUrl = node.querySelector?.('audio[src^="asset://"], video[src^="asset://"], iframe[src^="asset://"]') ||
(node.matches?.('audio[src^="asset://"], video[src^="asset://"], iframe[src^="asset://"]'));
const hasAssetUrl = node.querySelector?.(ASSET_URL_MEDIA_SELECTOR) ||
(node.matches?.(ASSET_URL_MEDIA_SELECTOR));
if (hasAssetUrl) {
hasNewMedia = true;
break;
Expand All @@ -646,6 +640,22 @@ var $exeTinyMCE = {
}); //End tinymce
},

prepareContentForEditorLoad: function (content) {
if (typeof content !== 'string' || !content.includes('asset://')) {
return content;
}

const assetManager = window.eXeLearning?.app?.project?._yjsBridge?.assetManager;
if (!assetManager?.resolveHTMLAssetsSync) {
return content;
}

return assetManager.resolveHTMLAssetsSync(content, {
usePlaceholder: true,
addTracking: true,
});
},

getSchema: function () {
var s = 'html5';
return s;
Expand Down Expand Up @@ -705,8 +715,9 @@ var $exeTinyMCE = {
},

/**
* Resolve asset:// URLs to blob:// URLs for audio/video elements in TinyMCE editor
* This allows media to play within the editor while keeping asset:// URLs for persistence
* Resolve asset:// URLs to blob:// URLs for editor-rendered media in TinyMCE.
* This allows images and media to render within the editor while keeping asset://
* URLs as the persisted format.
*
* NOTE: We intentionally DO NOT resolve iframes (PDFs) because:
* 1. TinyMCE strips custom attributes like data-asset-src when processing media elements
Expand All @@ -722,21 +733,22 @@ var $exeTinyMCE = {
const body = ed.getBody();
if (!body) return;

// Find audio, video, and iframe elements with asset:// URLs
const mediaElements = body.querySelectorAll('audio[src^="asset://"], video[src^="asset://"], iframe[src^="asset://"]');
// Find image, audio, video, and iframe elements with asset:// URLs
const mediaElements = body.querySelectorAll(ASSET_URL_MEDIA_SELECTOR);

for (const media of mediaElements) {
const assetUrl = media.getAttribute('src');
if (!assetUrl || !assetUrl.startsWith('asset://')) continue;

const isIframe = media.tagName.toLowerCase() === 'iframe';
const isImage = media.tagName.toLowerCase() === 'img';

// For audio/video: Skip if already resolved (has data-asset-src)
// For images/audio/video: Skip if already resolved (has data-asset-src)
// For iframes: Skip if src is already a blob URL (already resolved)
if (!isIframe && media.getAttribute('data-asset-src')) continue;
if (isIframe && media.getAttribute('src').startsWith('blob:')) continue;

// For audio/video: Store the original asset URL in data-asset-src
// For images/audio/video: Store the original asset URL in data-asset-src
// For iframes: DON'T add data-asset-src - TinyMCE preserves the URL via data-mce-p-src
// on the parent span.mce-preview-object
if (!isIframe) {
Expand Down Expand Up @@ -768,10 +780,18 @@ var $exeTinyMCE = {
});
}
} else {
// Resolve to blob URL asynchronously (for audio, video, PDF iframes)
if (isImage) {
media.setAttribute('src', PLACEHOLDER_IMAGE_DATA_URL);
media.setAttribute('data-mce-src', PLACEHOLDER_IMAGE_DATA_URL);
}

// Resolve to blob URL asynchronously (for images, audio, video, PDF iframes)
assetManager.resolveAssetURL(assetUrl).then(function(blobUrl) {
if (blobUrl) {
media.setAttribute('src', blobUrl);
if (isImage) {
media.setAttribute('data-mce-src', blobUrl);
}
}
}).catch(function(err) {
console.warn('[TinyMCE] Failed to resolve asset URL:', assetUrl, err);
Expand Down
Loading
Loading