This commit is contained in:
Markos Gogoulos
2026-02-10 19:08:35 +02:00
parent 166fb5c00b
commit 467e273ff5
6 changed files with 84 additions and 349 deletions

View File

@@ -2,186 +2,16 @@
A TinyMCE editor plugin for Moodle that provides media embedding capabilities with MediaCMS/LTI integration.
## Plugin Information
## Build Information
- **Component:** `tiny_mediacms`
- **Version:** See `version.php`
- **Requires:** Moodle 4.5+ (2024100100)
## Directory Structure
1. Get and extract Moodle 5.1
2. cp -r lms-plugins/mediacms-moodle/tiny/mediacms/ moodle/public/lib/editor/tiny/plugins/
3. nvm use 22 && cd moodle/public && npm install
4. npx grunt amd --root=lib/editor/tiny/plugins/mediacms
# i've noticed that this fails, so this should work: npx grunt amd
```
mediacms/
├── amd/
│ ├── src/ # JavaScript source files (ES6 modules)
│ │ ├── plugin.js # Main plugin entry point
│ │ ├── commands.js # Editor commands
│ │ ├── configuration.js # Plugin configuration
│ │ ├── iframeembed.js # Iframe embedding logic
│ │ ├── iframemodal.js # Iframe modal UI
│ │ ├── autoconvert.js # URL auto-conversion
│ │ ├── embed.js # Media embedding
│ │ ├── embedmodal.js # Embed modal UI
│ │ ├── image.js # Image handling
│ │ ├── imagemodal.js # Image modal UI
│ │ ├── imageinsert.js # Image insertion
│ │ ├── imagedetails.js # Image details panel
│ │ ├── imagehelpers.js # Image utility functions
│ │ ├── manager.js # File manager
│ │ ├── options.js # Plugin options
│ │ ├── selectors.js # DOM selectors
│ │ ├── common.js # Shared utilities
│ │ └── usedfiles.js # Track used files
│ └── build/ # Compiled/minified files (generated)
├── classes/ # PHP classes
├── lang/ # Language strings
│ └── en/
│ └── tiny_mediacms.php
├── templates/ # Mustache templates
├── styles.css # Plugin styles
├── settings.php # Admin settings
└── version.php # Plugin version
```
5. To test the output:
cp lib/editor/tiny/plugins/mediacms/* ../../lms-plugins/mediacms-moodle/tiny/mediacms/ -r
6. Then copy to Moodle server and purge caches
## Building JavaScript (AMD Modules)
When you modify JavaScript files in `amd/src/`, you must rebuild the minified files in `amd/build/`.
### Prerequisites
Make sure you have Node.js installed and have run `npm install` in the Moodle root directory:
```bash
cd /path/to/moodle/public
npm install
```
### Build Commands
#### Build all AMD modules (entire Moodle):
```bash
cd /path/to/moodle/public
npx grunt amd
```
#### Build only this plugin's AMD modules:
```bash
cd /path/to/moodle/public
npx grunt amd --root=lib/editor/tiny/plugins/mediacms
```
#### Watch for changes (auto-rebuild):
```bash
cd /path/to/moodle/public
npx grunt watch --root=lib/editor/tiny/plugins/mediacms
```
#### Force build (ignore warnings):
```bash
cd /path/to/moodle/public
npx grunt amd --force --root=lib/editor/tiny/plugins/mediacms
```
### Build Output
After running grunt, the following files are generated in `amd/build/`:
- `*.min.js` - Minified JavaScript files
- `*.min.js.map` - Source maps for debugging
## Development Mode (Skip Building)
For faster development, you can skip building by enabling developer mode in Moodle's `config.php`:
```php
// Add these lines to config.php
$CFG->debugdeveloper = true;
$CFG->cachejs = false;
```
This tells Moodle to load the unminified source files directly from `amd/src/` instead of `amd/build/`.
**Note:** Always build before committing or deploying to production!
## Purging Caches
After making changes, you may need to purge Moodle caches:
### Via CLI (Docker):
```bash
docker compose exec moodle php /var/www/html/public/admin/cli/purge_caches.php
```
### Via CLI (Local):
```bash
php admin/cli/purge_caches.php
```
### Via Web:
Visit: `http://your-moodle-site/admin/purgecaches.php`
## What Needs Cache Purging?
| File Type | Cache Purge Needed? |
|-----------|---------------------|
| `amd/src/*.js` | No (if `$CFG->cachejs = false`) |
| `amd/build/*.min.js` | Yes |
| `lang/en/*.php` | Yes |
| `templates/*.mustache` | Yes |
| `styles.css` | Yes |
| `classes/*.php` | Usually no |
| `settings.php` | Yes |
## Troubleshooting
### Changes not appearing?
1. **JavaScript changes:**
- Rebuild AMD modules: `npx grunt amd --root=lib/editor/tiny/plugins/mediacms`
- Hard refresh browser: `Cmd+Shift+R` (Mac) / `Ctrl+Shift+R` (Windows/Linux)
- Check browser console for errors
2. **Language strings:**
- Purge Moodle caches
3. **Templates:**
- Purge Moodle caches
4. **Styles:**
- Purge Moodle caches
- Hard refresh browser
### Grunt errors?
```bash
# Make sure dependencies are installed
cd /path/to/moodle/public
npm install
# Try with force flag
npx grunt amd --force --root=lib/editor/tiny/plugins/mediacms
```
### ESLint errors?
Fix linting issues or use:
```bash
npx grunt amd --force --root=lib/editor/tiny/plugins/mediacms
```
## Related Documentation
- [AUTOCONVERT.md](./AUTOCONVERT.md) - URL auto-conversion feature documentation
- [LTI_INTEGRATION.md](./LTI_INTEGRATION.md) - LTI integration documentation
## License
GNU GPL v3 or later - http://www.gnu.org/copyleft/gpl.html

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -694,6 +694,9 @@ export default class IframeEmbed {
// Iframe library event listeners
this.registerIframeLibraryEventListeners(root);
// Since My Media tab is now default/active, load it immediately on modal open
setTimeout(() => this.handleIframeLibraryTabClick(root), 100);
}
/**
@@ -796,24 +799,6 @@ export default class IframeEmbed {
* @param {HTMLElement} root - Modal root element
*/
handleIframeLibraryTabClick(root) {
const form = root.querySelector(Selectors.IFRAME.elements.form);
const pane = form.querySelector(
Selectors.IFRAME.elements.paneIframeLibrary,
);
const iframeEl = pane
? pane.querySelector(Selectors.IFRAME.elements.iframeLibraryFrame)
: null;
// eslint-disable-next-line no-console
console.log(
'handleIframeLibraryTabClick called, iframeLibraryLoaded:',
this.iframeLibraryLoaded,
'iframe src:',
iframeEl ? iframeEl.src : 'no iframe',
'pane:',
pane,
);
// Always refetch content when tab is clicked (no caching)
// Reset the loaded state to ensure fresh content is fetched
this.iframeLibraryLoaded = false;
@@ -827,13 +812,9 @@ export default class IframeEmbed {
*/
loadIframeLibrary(root) {
const ltiConfig = getLti(this.editor);
// eslint-disable-next-line no-console
console.log('loadIframeLibrary called, LTI config:', ltiConfig);
// Check if LTI is configured with a content item URL
if (ltiConfig?.contentItemUrl) {
this.loadIframeLibraryViaLti(root, ltiConfig);
this.loadIframeLibraryViaLti(root);
} else {
// Fallback to static URL if LTI not configured
this.loadIframeLibraryStatic(root);
@@ -847,21 +828,14 @@ export default class IframeEmbed {
* tool's content selection interface (e.g., /lti/select-media/).
*
* @param {HTMLElement} root - Modal root element
* @param {Object} ltiConfig - LTI configuration
*/
loadIframeLibraryViaLti(root, ltiConfig) {
// eslint-disable-next-line no-console
console.log('loadIframeLibraryViaLti called, config:', ltiConfig);
loadIframeLibraryViaLti(root) {
const form = root.querySelector(Selectors.IFRAME.elements.form);
const pane = form.querySelector(
Selectors.IFRAME.elements.paneIframeLibrary,
);
if (!pane) {
// eslint-disable-next-line no-console
console.log('paneIframeLibrary not found!');
return;
if (!pane) { return;
}
const placeholderEl = pane.querySelector(
@@ -874,10 +848,7 @@ export default class IframeEmbed {
Selectors.IFRAME.elements.iframeLibraryFrame,
);
if (!iframeEl) {
// eslint-disable-next-line no-console
console.log('iframeEl not found!');
return;
if (!iframeEl) { return;
}
// Hide placeholder, show loading state
@@ -890,10 +861,7 @@ export default class IframeEmbed {
iframeEl.classList.add('d-none');
// Set up load listener - note: this may fire multiple times during LTI redirects
const loadHandler = () => {
// eslint-disable-next-line no-console
console.log('LTI iframe loaded');
this.handleIframeLibraryLoad(root);
const loadHandler = () => { this.handleIframeLibraryLoad(root);
};
iframeEl.addEventListener('load', loadHandler);
@@ -902,12 +870,8 @@ export default class IframeEmbed {
// 1. contentitem.php initiates OIDC login
// 2. LTI provider authenticates
// 3. Moodle sends LtiDeepLinkingRequest
// 4. Tool provider shows content selection interface (e.g., /lti/select-media/)
// eslint-disable-next-line no-console
console.log(
'Setting iframe src to LTI content item URL:',
ltiConfig.contentItemUrl,
);
// 4. Tool provider shows content selection interface
const ltiConfig = getLti(this.editor);
iframeEl.src = ltiConfig.contentItemUrl;
}
@@ -917,21 +881,12 @@ export default class IframeEmbed {
* @param {HTMLElement} root - Modal root element
*/
loadIframeLibraryStatic(root) {
// eslint-disable-next-line no-console
console.log(
'loadIframeLibraryStatic called, URL:',
this.iframeLibraryUrl,
);
const form = root.querySelector(Selectors.IFRAME.elements.form);
const pane = form.querySelector(
Selectors.IFRAME.elements.paneIframeLibrary,
);
if (!pane) {
// eslint-disable-next-line no-console
console.log('paneIframeLibrary not found!');
return;
if (!pane) { return;
}
const placeholderEl = pane.querySelector(
@@ -943,14 +898,7 @@ export default class IframeEmbed {
const iframeEl = pane.querySelector(
Selectors.IFRAME.elements.iframeLibraryFrame,
);
// eslint-disable-next-line no-console
console.log('Elements found:', { placeholderEl, loadingEl, iframeEl });
if (!iframeEl) {
// eslint-disable-next-line no-console
console.log('iframeEl not found!');
return;
if (!iframeEl) { return;
}
// Hide placeholder, show loading state
@@ -963,10 +911,7 @@ export default class IframeEmbed {
iframeEl.classList.add('d-none');
// Set up load listener before setting src
const loadHandler = () => {
// eslint-disable-next-line no-console
console.log('iframe loaded, src:', iframeEl.src);
// Only handle if the src matches our target URL
const loadHandler = () => { // Only handle if the src matches our target URL
if (iframeEl.src === this.iframeLibraryUrl) {
this.handleIframeLibraryLoad(root);
// Remove the listener after successful load
@@ -976,10 +921,7 @@ export default class IframeEmbed {
iframeEl.addEventListener('load', loadHandler);
// Set the iframe source
iframeEl.src = this.iframeLibraryUrl;
// eslint-disable-next-line no-console
console.log('iframe src set to:', iframeEl.src);
}
iframeEl.src = this.iframeLibraryUrl; }
/**
* Handle iframe library load event.
@@ -987,9 +929,6 @@ export default class IframeEmbed {
* @param {HTMLElement} root - Modal root element
*/
handleIframeLibraryLoad(root) {
// eslint-disable-next-line no-console
console.log('handleIframeLibraryLoad called');
const form = root.querySelector(Selectors.IFRAME.elements.form);
const pane = form.querySelector(
Selectors.IFRAME.elements.paneIframeLibrary,
@@ -1031,29 +970,14 @@ export default class IframeEmbed {
* @param {MessageEvent} event - The message event
*/
handleIframeLibraryMessage(root, event) {
// eslint-disable-next-line no-console
console.log(
'handleIframeLibraryMessage received:',
event.data,
'from origin:',
event.origin,
);
const data = event.data;
if (!data) {
return;
}
// Handle custom videoSelected message format (from static iframe or custom MediaCMS implementation)
// Handle custom videoSelected message format
if (data.type === 'videoSelected' && data.embedUrl) {
// eslint-disable-next-line no-console
console.log(
'Video selected (videoSelected):',
data.embedUrl,
'videoId:',
data.videoId,
);
this.selectIframeLibraryVideo(root, data.embedUrl, data.videoId);
return;
}
@@ -1062,25 +986,14 @@ export default class IframeEmbed {
if (
data.type === 'ltiDeepLinkingResponse' ||
data.messageType === 'LtiDeepLinkingResponse'
) {
// eslint-disable-next-line no-console
console.log('LTI Deep Linking response received:', data);
const contentItems = data.content_items || data.contentItems || [];
) { const contentItems = data.content_items || data.contentItems || [];
if (contentItems.length > 0) {
const item = contentItems[0];
// Extract embed URL from the content item
const embedUrl =
item.url || item.embed_url || item.embedUrl || '';
const videoId = item.id || item.mediaId || '';
if (embedUrl) {
// eslint-disable-next-line no-console
console.log(
'Video selected (LTI):',
embedUrl,
'videoId:',
videoId,
);
this.selectIframeLibraryVideo(root, embedUrl, videoId);
if (embedUrl) { this.selectIframeLibraryVideo(root, embedUrl, videoId);
}
}
return;
@@ -1090,15 +1003,7 @@ export default class IframeEmbed {
if (data.action === 'selectMedia' || data.action === 'mediaSelected') {
const embedUrl = data.embedUrl || data.url || '';
const videoId = data.mediaId || data.videoId || data.id || '';
if (embedUrl) {
// eslint-disable-next-line no-console
console.log(
'Video selected (mediaSelected):',
embedUrl,
'videoId:',
videoId,
);
this.selectIframeLibraryVideo(root, embedUrl, videoId);
if (embedUrl) { this.selectIframeLibraryVideo(root, embedUrl, videoId);
}
return;
}
@@ -1118,7 +1023,13 @@ export default class IframeEmbed {
const urlInput = form.querySelector(Selectors.IFRAME.elements.url);
urlInput.value = embedUrl;
// Switch to the URL tab using our method
// Show the Configure tab (it starts hidden)
const configureTabItem = root.querySelector('.tiny_iframecms_tab_url_item');
if (configureTabItem) {
configureTabItem.style.display = '';
}
// Switch to the Configure tab to show embed options
this.switchToUrlTab(root);
// Update the preview

View File

@@ -132,15 +132,15 @@ $string['aspectratio_1_1'] = '1:1';
$string['aspectratio_custom'] = 'Custom';
$string['dimensions'] = 'Dimensions';
$string['preview'] = 'Preview';
$string['insertiframe'] = 'Insert video';
$string['insertiframe'] = 'Insert';
$string['updateiframe'] = 'Update video';
$string['removeiframe'] = 'Remove video';
$string['removeiframeconfirm'] = 'Are you sure you want to remove this video from the editor?';
// Iframe modal tabs.
$string['tabembedurl'] = 'Embed URL';
$string['tabembedurl'] = 'Configure';
$string['tabvideolibrary'] = 'Video Library';
$string['tabvideolibraryiframe'] = 'Media Library';
$string['tabvideolibraryiframe'] = 'My Media';
// Video library strings.
$string['librarysearchplaceholder'] = 'Search videos...';

View File

@@ -31,66 +31,28 @@
<form class="tiny_iframecms_form" id="{{elementid}}_tiny_iframecms_form">
<!-- Tab Navigation -->
<ul class="nav nav-tabs mb-3 tiny_iframecms_tabs" role="tablist">
<!-- My Media tab (first, active by default) -->
<li class="nav-item" role="presentation">
<button class="nav-link active tiny_iframecms_tab_url_btn" id="{{elementid}}_tab_url"
data-bs-toggle="tab" data-bs-target="#{{elementid}}_pane_url"
type="button" role="tab" aria-controls="{{elementid}}_pane_url" aria-selected="true">
{{#str}} tabembedurl, tiny_mediacms {{/str}}
<button class="nav-link active tiny_iframecms_tab_iframe_library_btn" id="{{elementid}}_tab_iframe_library"
data-bs-toggle="tab" data-bs-target="#{{elementid}}_pane_iframe_library"
type="button" role="tab" aria-controls="{{elementid}}_pane_iframe_library" aria-selected="true">
{{#str}} tabvideolibraryiframe, tiny_mediacms {{/str}}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link tiny_iframecms_tab_iframe_library_btn" id="{{elementid}}_tab_iframe_library"
data-bs-toggle="tab" data-bs-target="#{{elementid}}_pane_iframe_library"
type="button" role="tab" aria-controls="{{elementid}}_pane_iframe_library" aria-selected="false">
{{#str}} tabvideolibraryiframe, tiny_mediacms {{/str}}
<!-- Configure tab (second, hidden initially) -->
<li class="nav-item tiny_iframecms_tab_url_item" role="presentation" style="display: none;">
<button class="nav-link tiny_iframecms_tab_url_btn" id="{{elementid}}_tab_url"
data-bs-toggle="tab" data-bs-target="#{{elementid}}_pane_url"
type="button" role="tab" aria-controls="{{elementid}}_pane_url" aria-selected="false">
{{#str}} tabembedurl, tiny_mediacms {{/str}}
</button>
</li>
</ul>
<!-- Tab Content -->
<div class="tab-content">
<!-- Tab 1: Embed URL (existing content) -->
<div class="tab-pane fade show active tiny_iframecms_pane_url" id="{{elementid}}_pane_url" role="tabpanel" aria-labelledby="{{elementid}}_tab_url">
<div class="container-fluid p-0">
<div class="row">
<!-- Left column: URL and Options -->
<div class="col-md-6">
<!-- URL Input -->
<div class="mb-3">
<label for="{{elementid}}_iframe_url" class="form-label font-weight-bold">
{{#str}} iframeurl, tiny_mediacms {{/str}}
</label>
<textarea
class="form-control tiny_iframecms_url"
id="{{elementid}}_iframe_url"
rows="3"
placeholder="{{#str}} iframeurlplaceholder, tiny_mediacms {{/str}}"
>{{url}}</textarea>
<div class="tiny_iframecms_url_warning text-danger small mt-1 d-none">
{{#str}} iframeurlinvalid, tiny_mediacms {{/str}}
</div>
</div>
{{> tiny_mediacms/iframe_embed_options }}
</div>
<!-- Right column: Preview -->
<div class="col-md-6">
<label class="form-label font-weight-bold">
{{#str}} preview, tiny_mediacms {{/str}}
</label>
<div class="tiny_iframecms_preview_container border rounded p-2 bg-light" style="min-height: 300px;">
<div class="tiny_iframecms_preview d-flex align-items-center justify-content-center text-muted" style="min-height: 280px;">
<span>{{#str}} iframeurlplaceholder, tiny_mediacms {{/str}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tab 2: Media Library -->
<div class="tab-pane fade tiny_iframecms_pane_iframe_library" id="{{elementid}}_pane_iframe_library" role="tabpanel" aria-labelledby="{{elementid}}_tab_iframe_library">
<!-- Tab 1: My Media (now first and active) -->
<div class="tab-pane fade show active tiny_iframecms_pane_iframe_library" id="{{elementid}}_pane_iframe_library" role="tabpanel" aria-labelledby="{{elementid}}_tab_iframe_library">
<div class="tiny_iframecms_iframe_library_container" style="min-height: 500px;">
<div class="tiny_iframecms_iframe_library_placeholder text-center py-5">
<p class="text-muted">{{#str}} libraryloading, tiny_mediacms {{/str}}</p>
@@ -110,6 +72,38 @@
</iframe>
</div>
</div>
<!-- Tab 2: Configure (now second) -->
<div class="tab-pane fade tiny_iframecms_pane_url" id="{{elementid}}_pane_url" role="tabpanel" aria-labelledby="{{elementid}}_tab_url">
<div class="container-fluid p-0">
<div class="row">
<!-- Left column: Options only (URL field removed) -->
<div class="col-md-6">
<!-- Hidden URL input (still needed for internal logic) -->
<textarea
class="form-control tiny_iframecms_url d-none"
id="{{elementid}}_iframe_url"
rows="3"
>{{url}}</textarea>
<div class="tiny_iframecms_url_warning text-danger small mt-1 d-none"></div>
{{> tiny_mediacms/iframe_embed_options }}
</div>
<!-- Right column: Preview -->
<div class="col-md-6">
<label class="form-label font-weight-bold">
{{#str}} preview, tiny_mediacms {{/str}}
</label>
<div class="tiny_iframecms_preview_container border rounded p-2 bg-light" style="min-height: 300px;">
<div class="tiny_iframecms_preview d-flex align-items-center justify-content-center text-muted" style="min-height: 280px;">
<span>{{#str}} iframeurlplaceholder, tiny_mediacms {{/str}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
{{/body}}