This commit is contained in:
Markos Gogoulos
2026-02-03 19:25:59 +02:00
parent 637e9eabea
commit 156e0bddd6
177 changed files with 17056 additions and 0 deletions

View File

@@ -0,0 +1,206 @@
# MediaCMS URL Auto-Convert Feature
This feature automatically converts pasted MediaCMS video URLs into embedded video players within the TinyMCE editor.
## Overview
When a user pastes a MediaCMS video URL like:
```
https://deic.mediacms.io/view?m=JpBd1Zvdl
```
It is automatically converted to an embedded video player:
```html
<div class="tiny-iframe-responsive" contenteditable="false">
<iframe
style="width: 100%; max-width: calc(100vh * 16 / 9); aspect-ratio: 16 / 9; display: block; margin: auto; border: 0;"
src="https://deic.mediacms.io/embed?m=JpBd1Zvdl&showTitle=1&showRelated=1&showUserAvatar=1&linkTitle=1"
allowfullscreen="allowfullscreen">
</iframe>
</div>
```
## Supported URL Formats
The auto-convert feature recognizes MediaCMS view URLs in this format:
- `https://[domain]/view?m=[VIDEO_ID]`
Examples:
- `https://deic.mediacms.io/view?m=JpBd1Zvdl`
- `https://your-mediacms-instance.com/view?m=abc123`
## Configuration
### Accessing Settings
1. Log in to Moodle as an administrator
2. Navigate to: **Site administration****Plugins****Text editors****TinyMCE editor****MediaCMS**
3. Scroll to the **Auto-convert MediaCMS URLs** section
### Available Settings
| Setting | Description | Default |
|---------|-------------|---------|
| **Enable auto-convert** | Turn the auto-convert feature on or off | Enabled |
| **MediaCMS base URL** | Restrict auto-conversion to a specific MediaCMS domain | Empty (allow all) |
| **Show video title** | Display the video title in the embedded player | Enabled |
| **Link video title** | Make the video title clickable, linking to the original video page | Enabled |
| **Show related videos** | Display related videos after the current video ends | Enabled |
| **Show user avatar** | Display the uploader's avatar in the embedded player | Enabled |
### Settings Location in Moodle
The settings are stored in the Moodle database under the `tiny_mediacms` plugin configuration:
- `tiny_mediacms/autoconvertenabled` - Enable/disable auto-convert
- `tiny_mediacms/autoconvert_baseurl` - MediaCMS base URL (e.g., https://deic.mediacms.io)
- `tiny_mediacms/autoconvert_showtitle` - Show title option
- `tiny_mediacms/autoconvert_linktitle` - Link title option
- `tiny_mediacms/autoconvert_showrelated` - Show related option
- `tiny_mediacms/autoconvert_showuseravatar` - Show user avatar option
### Base URL Configuration
The **MediaCMS base URL** setting controls which MediaCMS instances are recognized for auto-conversion:
- **Empty (default)**: Any MediaCMS URL will be auto-converted (e.g., URLs from any `*/view?m=*` pattern)
- **Specific URL**: Only URLs from the specified domain will be auto-converted
Example configurations:
- `https://deic.mediacms.io` - Only convert URLs from deic.mediacms.io
- `https://media.myuniversity.edu` - Only convert URLs from your institution's MediaCMS
## Technical Details
### File Structure
```
amd/src/
├── autoconvert.js # Main auto-convert module
├── plugin.js # Plugin initialization (imports autoconvert)
└── options.js # Configuration options definition
classes/
└── plugininfo.php # Passes PHP settings to JavaScript
settings.php # Admin settings page definition
lang/en/
└── tiny_mediacms.php # Language strings for settings
```
### How It Works
1. **Paste Detection**: The `autoconvert.js` module listens for `paste` events on the TinyMCE editor
2. **URL Validation**: When text is pasted, it checks if it matches the MediaCMS URL pattern
3. **HTML Generation**: If valid, it generates the responsive iframe HTML with configured options
4. **Content Insertion**: The original URL is replaced with the embedded video
### JavaScript Configuration
The settings are passed from PHP to JavaScript via the `plugininfo.php` class:
```php
protected static function get_autoconvert_configuration(): array {
$baseurl = get_config('tiny_mediacms', 'autoconvert_baseurl');
return [
'data' => [
'autoConvertEnabled' => (bool) get_config('tiny_mediacms', 'autoconvertenabled'),
'autoConvertBaseUrl' => !empty($baseurl) ? $baseurl : '',
'autoConvertOptions' => [
'showTitle' => (bool) get_config('tiny_mediacms', 'autoconvert_showtitle'),
'linkTitle' => (bool) get_config('tiny_mediacms', 'autoconvert_linktitle'),
'showRelated' => (bool) get_config('tiny_mediacms', 'autoconvert_showrelated'),
'showUserAvatar' => (bool) get_config('tiny_mediacms', 'autoconvert_showuseravatar'),
],
],
];
}
```
### Default Values (in options.js)
If PHP settings are not configured, the JavaScript uses these defaults:
```javascript
registerOption(dataName, {
processor: 'object',
"default": {
autoConvertEnabled: true,
autoConvertBaseUrl: '', // Empty = allow all MediaCMS domains
autoConvertOptions: {
showTitle: true,
linkTitle: true,
showRelated: true,
showUserAvatar: true,
},
},
});
```
## Customization
### Disabling Auto-Convert
To disable the feature entirely:
1. Go to the plugin settings (see "Accessing Settings" above)
2. Uncheck **Enable auto-convert**
3. Save changes
### Programmatic Configuration
You can also set these values directly in the database using Moodle's `set_config()` function:
```php
// Disable auto-convert
set_config('autoconvertenabled', 0, 'tiny_mediacms');
// Set the MediaCMS base URL (restrict to specific domain)
set_config('autoconvert_baseurl', 'https://deic.mediacms.io', 'tiny_mediacms');
// Customize embed options
set_config('autoconvert_showtitle', 1, 'tiny_mediacms');
set_config('autoconvert_linktitle', 0, 'tiny_mediacms');
set_config('autoconvert_showrelated', 0, 'tiny_mediacms');
set_config('autoconvert_showuseravatar', 1, 'tiny_mediacms');
```
### CLI Configuration
Using Moodle CLI:
```bash
# Enable auto-convert
php admin/cli/cfg.php --component=tiny_mediacms --name=autoconvertenabled --set=1
# Set the MediaCMS base URL
php admin/cli/cfg.php --component=tiny_mediacms --name=autoconvert_baseurl --set=https://deic.mediacms.io
# Disable showing related videos
php admin/cli/cfg.php --component=tiny_mediacms --name=autoconvert_showrelated --set=0
```
## Troubleshooting
### Auto-convert not working
1. **Check if enabled**: Verify the setting is enabled in plugin settings
2. **Clear caches**: Purge all caches (Site administration → Development → Purge all caches)
3. **Check URL format**: Ensure the URL matches the pattern `https://[domain]/view?m=[VIDEO_ID]`
4. **Browser console**: Check for JavaScript errors in the browser developer console
### Rebuilding JavaScript
If you modify the source files, rebuild using:
```bash
cd /path/to/moodle
npx grunt amd --root=public/lib/editor/tiny/plugins/mediacms
```
Note: Requires Node.js 22.x or compatible version as specified in Moodle's requirements.
## Version History
- **1.0.0** - Initial implementation of auto-convert feature

View File

@@ -0,0 +1,187 @@
# TinyMCE MediaCMS Plugin for Moodle
A TinyMCE editor plugin for Moodle that provides media embedding capabilities with MediaCMS/LTI integration.
## Plugin Information
- **Component:** `tiny_mediacms`
- **Version:** See `version.php`
- **Requires:** Moodle 4.5+ (2024100100)
## Directory Structure
```
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
```
## 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

View File

@@ -0,0 +1,15 @@
define("tiny_mediacms/autoconvert",["exports","./options"],(function(_exports,_options){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setupAutoConvert=_exports.isMediaCMSUrl=_exports.convertToEmbed=void 0;
/**
* Tiny MediaCMS Auto-convert module.
*
* This module automatically converts pasted MediaCMS URLs into embedded videos.
* When a user pastes a MediaCMS video URL (e.g., https://deic.mediacms.io/view?m=JpBd1Zvdl),
* it will be automatically converted to an iframe embed.
*
* @module tiny_mediacms/autoconvert
* @copyright 2024
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const MEDIACMS_VIEW_URL_PATTERN=/^(https?:\/\/[^\/]+)\/view\?m=([a-zA-Z0-9_-]+)$/,parseMediaCMSUrl=text=>{if(!text||"string"!=typeof text)return null;const trimmed=text.trim(),match=trimmed.match(MEDIACMS_VIEW_URL_PATTERN);return match?{baseUrl:match[1],videoId:match[2],originalUrl:trimmed}:null},isDomainAllowed=(parsed,config)=>{const configuredBaseUrl=config.autoConvertBaseUrl||config.mediacmsBaseUrl;if(!configuredBaseUrl)return!0;try{const configuredUrl=new URL(configuredBaseUrl),pastedUrl=new URL(parsed.baseUrl);return configuredUrl.host===pastedUrl.host}catch(e){return!0}},generateEmbedHtml=function(parsed){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const embedUrl=new URL("".concat(parsed.baseUrl,"/embed"));embedUrl.searchParams.set("m",parsed.videoId),embedUrl.searchParams.set("showTitle",!1!==options.showTitle?"1":"0"),embedUrl.searchParams.set("showRelated",!1!==options.showRelated?"1":"0"),embedUrl.searchParams.set("showUserAvatar",!1!==options.showUserAvatar?"1":"0"),embedUrl.searchParams.set("linkTitle",!1!==options.linkTitle?"1":"0");const html='<iframe src="'.concat(embedUrl.toString(),'" ')+'style="width: 100%; aspect-ratio: 16 / 9; display: block; border: 0;" allowfullscreen="allowfullscreen"></iframe>';return html};_exports.setupAutoConvert=editor=>{const config=(0,_options.getData)(editor)||{};!1!==config.autoConvertEnabled&&(editor.on("paste",(e=>{handlePasteEvent(editor,e,config)})),editor.on("input",(e=>{handleInputEvent(editor,e,config)})))};const handlePasteEvent=(editor,e,config)=>{const clipboardData=e.clipboardData||window.clipboardData;if(!clipboardData)return;const text=clipboardData.getData("text/plain")||clipboardData.getData("text");if(!text)return;const parsed=parseMediaCMSUrl(text);if(!parsed)return;if(!isDomainAllowed(parsed,config))return;e.preventDefault(),e.stopPropagation();const embedHtml=generateEmbedHtml(parsed,config.autoConvertOptions||{});setTimeout((()=>{editor.insertContent(embedHtml),editor.selection.collapse(!1)}),0)},handleInputEvent=(editor,e,config)=>{if("insertFromPaste"!==e.inputType&&"insertText"!==e.inputType)return;const node=editor.selection.getNode();if(!node||"P"!==node.nodeName)return;const text=node.textContent||"",parsed=parseMediaCMSUrl(text);if(!parsed||!isDomainAllowed(parsed,config))return;const trimmedHtml=node.innerHTML.trim();if(trimmedHtml!==text.trim()&&!trimmedHtml.startsWith(text.trim()))return;const embedHtml=generateEmbedHtml(parsed,config.autoConvertOptions||{});setTimeout((()=>{const currentText=node.textContent||"",currentParsed=parseMediaCMSUrl(currentText);currentParsed&&currentParsed.originalUrl===parsed.originalUrl&&(editor.selection.select(node),editor.insertContent(embedHtml))}),100)};_exports.isMediaCMSUrl=text=>null!==parseMediaCMSUrl(text);_exports.convertToEmbed=function(url){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const parsed=parseMediaCMSUrl(url);return parsed?generateEmbedHtml(parsed,options):null}}));
//# sourceMappingURL=autoconvert.min.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
define("tiny_mediacms/common",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={pluginName:"tiny_mediacms/plugin",component:"tiny_mediacms",iframeButtonName:"tiny_mediacms_iframe",iframeMenuItemName:"tiny_mediacms_iframe",iframeIcon:"tiny_mediacms_iframe"},_exports.default}));
//# sourceMappingURL=common.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"common.min.js","sources":["../src/common.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Media common values.\n *\n * @module tiny_mediacms/common\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n pluginName: 'tiny_mediacms/plugin',\n component: 'tiny_mediacms',\n iframeButtonName: 'tiny_mediacms_iframe',\n iframeMenuItemName: 'tiny_mediacms_iframe',\n iframeIcon: 'tiny_mediacms_iframe',\n};\n"],"names":["pluginName","component","iframeButtonName","iframeMenuItemName","iframeIcon"],"mappings":"sKAuBe,CACXA,WAAY,uBACZC,UAAW,gBACXC,iBAAkB,uBAClBC,mBAAoB,uBACpBC,WAAY"}

View File

@@ -0,0 +1,3 @@
define("tiny_mediacms/configuration",["exports","./common","editor_tiny/utils"],(function(_exports,_common,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.configure=void 0;_exports.configure=instanceConfig=>{return{contextmenu:(0,_utils.addContextmenuItem)(instanceConfig.contextmenu,_common.iframeButtonName),menu:(menu=instanceConfig.menu,menu.insert.items="".concat(_common.iframeMenuItemName," ").concat(menu.insert.items),menu),toolbar:(toolbar=instanceConfig.toolbar,toolbar.map((section=>("content"===section.name&&section.items.unshift(_common.iframeButtonName),section))))};var toolbar,menu}}));
//# sourceMappingURL=configuration.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"configuration.min.js","sources":["../src/configuration.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Media configuration.\n *\n * @module tiny_mediacms/configuration\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {\n iframeButtonName,\n iframeMenuItemName,\n} from './common';\nimport {\n addContextmenuItem,\n} from 'editor_tiny/utils';\n\nconst configureMenu = (menu) => {\n // Add the Iframe Embed to the insert menu.\n menu.insert.items = `${iframeMenuItemName} ${menu.insert.items}`;\n\n return menu;\n};\n\nconst configureToolbar = (toolbar) => {\n // The toolbar contains an array of named sections.\n // The Moodle integration ensures that there is a section called 'content'.\n\n return toolbar.map((section) => {\n if (section.name === 'content') {\n // Insert the iframe button at the start of it.\n section.items.unshift(iframeButtonName);\n }\n\n return section;\n });\n};\n\nexport const configure = (instanceConfig) => {\n // Update the instance configuration to add the Iframe Embed menu option to the menus and toolbars.\n return {\n contextmenu: addContextmenuItem(instanceConfig.contextmenu, iframeButtonName),\n menu: configureMenu(instanceConfig.menu),\n toolbar: configureToolbar(instanceConfig.toolbar),\n };\n};\n"],"names":["instanceConfig","contextmenu","iframeButtonName","menu","insert","items","iframeMenuItemName","toolbar","map","section","name","unshift"],"mappings":"wNAoD0BA,uBAEf,CACHC,aAAa,6BAAmBD,eAAeC,YAAaC,0BAC5DC,MAzBeA,KAyBKH,eAAeG,KAvBvCA,KAAKC,OAAOC,gBAAWC,uCAAsBH,KAAKC,OAAOC,OAElDF,MAsBHI,SAnBkBA,QAmBQP,eAAeO,QAftCA,QAAQC,KAAKC,UACK,YAAjBA,QAAQC,MAERD,QAAQJ,MAAMM,QAAQT,0BAGnBO,aAVWF,IAAAA,QAPHJ"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
define("tiny_mediacms/embedmodal",["exports","core/modal","./common"],(function(_exports,_modal,_common){var obj;function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=(obj=_modal)&&obj.__esModule?obj:{default:obj};class EmbedModal extends _modal.default{registerEventListeners(){super.registerEventListeners(),this.registerCloseOnSave(),this.registerCloseOnCancel()}configure(modalConfig){modalConfig.large=!0,modalConfig.removeOnClose=!0,modalConfig.show=!0,super.configure(modalConfig)}}return _exports.default=EmbedModal,_defineProperty(EmbedModal,"TYPE","".concat(_common.component,"/modal")),_defineProperty(EmbedModal,"TEMPLATE","".concat(_common.component,"/embed_media_modal")),_exports.default}));
//# sourceMappingURL=embedmodal.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"embedmodal.min.js","sources":["../src/embedmodal.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Embedded Media Management Modal for Tiny.\n *\n * @module tiny_mediacms/embedmodal\n * @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from 'core/modal';\nimport {component} from './common';\n\nexport default class EmbedModal extends Modal {\n static TYPE = `${component}/modal`;\n static TEMPLATE = `${component}/embed_media_modal`;\n\n registerEventListeners() {\n // Call the parent registration.\n super.registerEventListeners();\n\n // Register to close on save/cancel.\n this.registerCloseOnSave();\n this.registerCloseOnCancel();\n }\n\n configure(modalConfig) {\n modalConfig.large = true;\n modalConfig.removeOnClose = true;\n modalConfig.show = true;\n\n super.configure(modalConfig);\n }\n}\n"],"names":["EmbedModal","Modal","registerEventListeners","registerCloseOnSave","registerCloseOnCancel","configure","modalConfig","large","removeOnClose","show","component"],"mappings":"iaA0BqBA,mBAAmBC,eAIpCC,+BAEUA,8BAGDC,2BACAC,wBAGTC,UAAUC,aACNA,YAAYC,OAAQ,EACpBD,YAAYE,eAAgB,EAC5BF,YAAYG,MAAO,QAEbJ,UAAUC,iEAlBHN,4BACAU,6CADAV,gCAEIU"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
define("tiny_mediacms/iframemodal",["exports","core/modal","./common"],(function(_exports,_modal,_common){var obj;function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=(obj=_modal)&&obj.__esModule?obj:{default:obj};class IframeModal extends _modal.default{registerEventListeners(){super.registerEventListeners(),this.registerCloseOnSave(),this.registerCloseOnCancel()}configure(modalConfig){modalConfig.large=!0,modalConfig.removeOnClose=!0,modalConfig.show=!0,super.configure(modalConfig)}}return _exports.default=IframeModal,_defineProperty(IframeModal,"TYPE","".concat(_common.component,"/iframemodal")),_defineProperty(IframeModal,"TEMPLATE","".concat(_common.component,"/iframe_embed_modal")),_exports.default}));
//# sourceMappingURL=iframemodal.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"iframemodal.min.js","sources":["../src/iframemodal.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Iframe Embed Modal for Tiny Media2.\n *\n * @module tiny_mediacms/iframemodal\n * @copyright 2024\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from 'core/modal';\nimport {component} from './common';\n\nexport default class IframeModal extends Modal {\n static TYPE = `${component}/iframemodal`;\n static TEMPLATE = `${component}/iframe_embed_modal`;\n\n registerEventListeners() {\n // Call the parent registration.\n super.registerEventListeners();\n\n // Register to close on save/cancel.\n this.registerCloseOnSave();\n this.registerCloseOnCancel();\n }\n\n configure(modalConfig) {\n modalConfig.large = true;\n modalConfig.removeOnClose = true;\n modalConfig.show = true;\n\n super.configure(modalConfig);\n }\n}\n"],"names":["IframeModal","Modal","registerEventListeners","registerCloseOnSave","registerCloseOnCancel","configure","modalConfig","large","removeOnClose","show","component"],"mappings":"kaA0BqBA,oBAAoBC,eAIrCC,+BAEUA,8BAGDC,2BACAC,wBAGTC,UAAUC,aACNA,YAAYC,OAAQ,EACpBD,YAAYE,eAAgB,EAC5BF,YAAYG,MAAO,QAEbJ,UAAUC,kEAlBHN,6BACAU,mDADAV,iCAEIU"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,10 @@
define("tiny_mediacms/imagehelpers",["exports","core/templates"],(function(_exports,_templates){var obj;
/**
* Tiny media plugin image helpers.
*
* @module tiny_mediacms/imagehelpers
* @copyright 2024 Meirza <meirza.arson@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.showElements=_exports.isPercentageValue=_exports.hideElements=_exports.footerImageInsert=_exports.footerImageDetails=_exports.bodyImageInsert=_exports.bodyImageDetails=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};_exports.bodyImageInsert=async(templateContext,root)=>_templates.default.renderForPromise("tiny_mediacms/insert_image_modal_insert",{...templateContext}).then((_ref=>{let{html:html,js:js}=_ref;_templates.default.replaceNodeContents(root.querySelector(".tiny_imagecms_body_template"),html,js)})).catch((error=>{window.console.log(error)}));_exports.footerImageInsert=async(templateContext,root)=>_templates.default.renderForPromise("tiny_mediacms/insert_image_modal_insert_footer",{...templateContext}).then((_ref2=>{let{html:html,js:js}=_ref2;_templates.default.replaceNodeContents(root.querySelector(".tiny_imagecms_footer_template"),html,js)})).catch((error=>{window.console.log(error)}));_exports.bodyImageDetails=async(templateContext,root)=>_templates.default.renderForPromise("tiny_mediacms/insert_image_modal_details",{...templateContext}).then((_ref3=>{let{html:html,js:js}=_ref3;_templates.default.replaceNodeContents(root.querySelector(".tiny_imagecms_body_template"),html,js)})).catch((error=>{window.console.log(error)}));_exports.footerImageDetails=async(templateContext,root)=>_templates.default.renderForPromise("tiny_mediacms/insert_image_modal_details_footer",{...templateContext}).then((_ref4=>{let{html:html,js:js}=_ref4;_templates.default.replaceNodeContents(root.querySelector(".tiny_imagecms_footer_template"),html,js)})).catch((error=>{window.console.log(error)}));_exports.showElements=(elements,root)=>{if(elements instanceof Array)elements.forEach((elementSelector=>{const element=root.querySelector(elementSelector);element&&element.classList.remove("d-none")}));else{const element=root.querySelector(elements);element&&element.classList.remove("d-none")}};_exports.hideElements=(elements,root)=>{if(elements instanceof Array)elements.forEach((elementSelector=>{const element=root.querySelector(elementSelector);element&&element.classList.add("d-none")}));else{const element=root.querySelector(elements);element&&element.classList.add("d-none")}};_exports.isPercentageValue=value=>value.match(/\d+%/)}));
//# sourceMappingURL=imagehelpers.min.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
define("tiny_mediacms/imagemodal",["exports","core/modal","./common"],(function(_exports,_modal,_common){var obj;function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=(obj=_modal)&&obj.__esModule?obj:{default:obj};class ImageModal extends _modal.default{registerEventListeners(){super.registerEventListeners(),this.registerCloseOnSave(),this.registerCloseOnCancel()}configure(modalConfig){modalConfig.large=!0,modalConfig.removeOnClose=!0,modalConfig.show=!0,super.configure(modalConfig)}}return _exports.default=ImageModal,_defineProperty(ImageModal,"TYPE","".concat(_common.component,"/imagemodal")),_defineProperty(ImageModal,"TEMPLATE","".concat(_common.component,"/insert_image_modal")),ImageModal.registerModalType(),_exports.default}));
//# sourceMappingURL=imagemodal.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"imagemodal.min.js","sources":["../src/imagemodal.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Image Modal for Tiny.\n *\n * @module tiny_mediacms/imagemodal\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from 'core/modal';\nimport {component} from './common';\n\nexport default class ImageModal extends Modal {\n static TYPE = `${component}/imagemodal`;\n static TEMPLATE = `${component}/insert_image_modal`;\n\n registerEventListeners() {\n // Call the parent registration.\n super.registerEventListeners();\n\n // Register to close on save/cancel.\n this.registerCloseOnSave();\n this.registerCloseOnCancel();\n }\n\n configure(modalConfig) {\n modalConfig.large = true;\n modalConfig.removeOnClose = true;\n modalConfig.show = true;\n\n super.configure(modalConfig);\n }\n}\n\nImageModal.registerModalType();\n"],"names":["ImageModal","Modal","registerEventListeners","registerCloseOnSave","registerCloseOnCancel","configure","modalConfig","large","removeOnClose","show","component","registerModalType"],"mappings":"iaA0BqBA,mBAAmBC,eAIpCC,+BAEUA,8BAGDC,2BACAC,wBAGTC,UAAUC,aACNA,YAAYC,OAAQ,EACpBD,YAAYE,eAAgB,EAC5BF,YAAYG,MAAO,QAEbJ,UAAUC,iEAlBHN,4BACAU,kDADAV,gCAEIU,0CAoBzBV,WAAWW"}

View File

@@ -0,0 +1,3 @@
define("tiny_mediacms/manager",["exports","core/templates","core/str","core/modal","core/modal_events","./options","core/config"],(function(_exports,_templates,_str,_modal,ModalEvents,_options,_config){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=_interopRequireDefault(_templates),_modal=_interopRequireDefault(_modal),ModalEvents=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(ModalEvents),_config=_interopRequireDefault(_config);return _exports.default=class{constructor(editor){_defineProperty(this,"editor",null),_defineProperty(this,"area",null),this.editor=editor;const data=(0,_options.getData)(editor);this.area=data.params.area,this.area.itemid=data.fpoptions.image.itemid}async displayDialogue(){const modal=await _modal.default.create({large:!0,title:(0,_str.getString)("mediamanagerproperties","tiny_mediacms"),body:_templates.default.render("tiny_mediacms/mm2_iframe",{src:this.getIframeURL()}),removeOnClose:!0,show:!0});return modal.getRoot().on(ModalEvents.bodyRendered,(()=>{this.selectFirstElement()})),document.querySelector(".modal-lg").style.cssText="max-width: 850px",modal}selectFirstElement(){const iframe=document.getElementById("mm2-iframe");iframe.addEventListener("load",(function(){let intervalId=setInterval((function(){const iDocument=iframe.contentWindow.document;if(iDocument.querySelector(".filemanager")){const firstFocusableElement=iDocument.querySelector(".fp-navbar a:not([disabled])");firstFocusableElement&&firstFocusableElement.focus(),clearInterval(intervalId)}}),200)}))}getIframeURL(){const url=new URL("".concat(_config.default.wwwroot,"/lib/editor/tiny/plugins/mediacms/manage.php"));url.searchParams.append("elementid",this.editor.getElement().id);for(const key in this.area)url.searchParams.append(key,this.area[key]);return url.toString()}},_exports.default}));
//# sourceMappingURL=manager.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"manager.min.js","sources":["../src/manager.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Media Manager plugin class for Moodle.\n *\n * @module tiny_mediacms/manager\n * @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport {getString} from 'core/str';\nimport Modal from 'core/modal';\nimport * as ModalEvents from 'core/modal_events';\nimport {getData} from './options';\nimport Config from 'core/config';\n\nexport default class MediaManager {\n\n editor = null;\n area = null;\n\n constructor(editor) {\n this.editor = editor;\n const data = getData(editor);\n this.area = data.params.area;\n this.area.itemid = data.fpoptions.image.itemid;\n }\n\n async displayDialogue() {\n const modal = await Modal.create({\n large: true,\n title: getString('mediamanagerproperties', 'tiny_mediacms'),\n body: Templates.render('tiny_mediacms/mm2_iframe', {\n src: this.getIframeURL()\n }),\n removeOnClose: true,\n show: true,\n });\n modal.getRoot().on(ModalEvents.bodyRendered, () => {\n this.selectFirstElement();\n });\n\n document.querySelector('.modal-lg').style.cssText = `max-width: 850px`;\n return modal;\n }\n\n // It will select the first element in the file manager.\n selectFirstElement() {\n const iframe = document.getElementById('mm2-iframe');\n iframe.addEventListener('load', function() {\n let intervalId = setInterval(function() {\n const iDocument = iframe.contentWindow.document;\n if (iDocument.querySelector('.filemanager')) {\n const firstFocusableElement = iDocument.querySelector('.fp-navbar a:not([disabled])');\n if (firstFocusableElement) {\n firstFocusableElement.focus();\n }\n clearInterval(intervalId);\n }\n }, 200);\n });\n }\n\n getIframeURL() {\n const url = new URL(`${Config.wwwroot}/lib/editor/tiny/plugins/mediacms/manage.php`);\n url.searchParams.append('elementid', this.editor.getElement().id);\n for (const key in this.area) {\n url.searchParams.append(key, this.area[key]);\n }\n return url.toString();\n }\n}\n"],"names":["constructor","editor","data","area","params","itemid","fpoptions","image","modal","Modal","create","large","title","body","Templates","render","src","this","getIframeURL","removeOnClose","show","getRoot","on","ModalEvents","bodyRendered","selectFirstElement","document","querySelector","style","cssText","iframe","getElementById","addEventListener","intervalId","setInterval","iDocument","contentWindow","firstFocusableElement","focus","clearInterval","url","URL","Config","wwwroot","searchParams","append","getElement","id","key","toString"],"mappings":"mmDAmCIA,YAAYC,sCAHH,kCACF,WAGEA,OAASA,aACRC,MAAO,oBAAQD,aAChBE,KAAOD,KAAKE,OAAOD,UACnBA,KAAKE,OAASH,KAAKI,UAAUC,MAAMF,qCAIlCG,YAAcC,eAAMC,OAAO,CAC7BC,OAAO,EACPC,OAAO,kBAAU,yBAA0B,iBAC3CC,KAAMC,mBAAUC,OAAO,2BAA4B,CAC/CC,IAAKC,KAAKC,iBAEdC,eAAe,EACfC,MAAM,WAEVZ,MAAMa,UAAUC,GAAGC,YAAYC,cAAc,UACpCC,wBAGTC,SAASC,cAAc,aAAaC,MAAMC,2BACnCrB,MAIXiB,2BACUK,OAASJ,SAASK,eAAe,cACvCD,OAAOE,iBAAiB,QAAQ,eACxBC,WAAaC,aAAY,iBACnBC,UAAYL,OAAOM,cAAcV,YACnCS,UAAUR,cAAc,gBAAiB,OACnCU,sBAAwBF,UAAUR,cAAc,gCAClDU,uBACAA,sBAAsBC,QAE1BC,cAAcN,eAEnB,QAIXf,qBACUsB,IAAM,IAAIC,cAAOC,gBAAOC,yDAC9BH,IAAII,aAAaC,OAAO,YAAa5B,KAAKhB,OAAO6C,aAAaC,QACzD,MAAMC,OAAO/B,KAAKd,KACnBqC,IAAII,aAAaC,OAAOG,IAAK/B,KAAKd,KAAK6C,aAEpCR,IAAIS"}

View File

@@ -0,0 +1,11 @@
define("tiny_mediacms/options",["exports","editor_tiny/options","./common"],(function(_exports,_options,_common){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.register=_exports.getPermissions=_exports.getLti=_exports.getImagePermissions=_exports.getEmbedPermissions=_exports.getData=void 0;
/**
* Options helper for Tiny Media plugin.
*
* @module tiny_mediacms/options
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const dataName=(0,_options.getPluginOptionName)(_common.pluginName,"data"),permissionsName=(0,_options.getPluginOptionName)(_common.pluginName,"permissions"),ltiName=(0,_options.getPluginOptionName)(_common.pluginName,"lti");_exports.register=editor=>{const registerOption=editor.options.register;registerOption(permissionsName,{processor:"object",default:{image:{filepicker:!1}}}),registerOption(dataName,{processor:"object",default:{mediacmsApiUrl:"",mediacmsBaseUrl:"",mediacmsPageSize:12,autoConvertEnabled:!0,autoConvertBaseUrl:"",autoConvertOptions:{showTitle:!0,linkTitle:!0,showRelated:!0,showUserAvatar:!0}}}),registerOption(ltiName,{processor:"object",default:{toolId:0,courseId:0,contentItemUrl:""}})};const getPermissions=editor=>editor.options.get(permissionsName);_exports.getPermissions=getPermissions;_exports.getImagePermissions=editor=>getPermissions(editor).image;_exports.getEmbedPermissions=editor=>getPermissions(editor).embed;_exports.getData=editor=>editor.options.get(dataName);_exports.getLti=editor=>editor.options.get(ltiName)}));
//# sourceMappingURL=options.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"options.min.js","sources":["../src/options.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Options helper for Tiny Media plugin.\n *\n * @module tiny_mediacms/options\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getPluginOptionName} from 'editor_tiny/options';\nimport {pluginName} from './common';\n\nconst dataName = getPluginOptionName(pluginName, 'data');\nconst permissionsName = getPluginOptionName(pluginName, 'permissions');\nconst ltiName = getPluginOptionName(pluginName, 'lti');\n\n/**\n * Register the options for the Tiny Media plugin.\n *\n * @param {TinyMCE} editor\n */\nexport const register = (editor) => {\n const registerOption = editor.options.register;\n\n registerOption(permissionsName, {\n processor: 'object',\n \"default\": {\n image: {\n filepicker: false,\n }\n },\n });\n\n registerOption(dataName, {\n processor: 'object',\n \"default\": {\n // MediaCMS video library configuration\n mediacmsApiUrl: '', // e.g., 'https://deic.mediacms.io/api/v1/media'\n mediacmsBaseUrl: '', // e.g., 'https://deic.mediacms.io'\n mediacmsPageSize: 12,\n // Auto-conversion settings\n autoConvertEnabled: true, // Enable/disable auto-conversion of pasted MediaCMS URLs\n autoConvertBaseUrl: '', // Base URL to restrict auto-conversion (empty = allow all MediaCMS domains)\n autoConvertOptions: {\n // Default embed options for auto-converted videos\n showTitle: true,\n linkTitle: true,\n showRelated: true,\n showUserAvatar: true,\n },\n },\n });\n\n registerOption(ltiName, {\n processor: 'object',\n \"default\": {\n // LTI configuration for MediaCMS iframe library\n toolId: 0, // LTI external tool ID\n courseId: 0, // Current course ID\n contentItemUrl: '', // URL to /mod/lti/contentitem.php for Deep Linking\n },\n });\n};\n\n/**\n * Get the permissions configuration for the Tiny Media plugin.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getPermissions = (editor) => editor.options.get(permissionsName);\n\n/**\n * Get the permissions configuration for the Tiny Media plugin.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getImagePermissions = (editor) => getPermissions(editor).image;\n\n/**\n * Get the permissions configuration for the Tiny Media plugin.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getEmbedPermissions = (editor) => getPermissions(editor).embed;\n\n/**\n * Get the data configuration for the Media Manager.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getData = (editor) => editor.options.get(dataName);\n\n/**\n * Get the LTI configuration for the MediaCMS iframe library.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getLti = (editor) => editor.options.get(ltiName);\n"],"names":["dataName","pluginName","permissionsName","ltiName","editor","registerOption","options","register","processor","image","filepicker","mediacmsApiUrl","mediacmsBaseUrl","mediacmsPageSize","autoConvertEnabled","autoConvertBaseUrl","autoConvertOptions","showTitle","linkTitle","showRelated","showUserAvatar","toolId","courseId","contentItemUrl","getPermissions","get","embed"],"mappings":";;;;;;;;MA0BMA,UAAW,gCAAoBC,mBAAY,QAC3CC,iBAAkB,gCAAoBD,mBAAY,eAClDE,SAAU,gCAAoBF,mBAAY,yBAOvBG,eACfC,eAAiBD,OAAOE,QAAQC,SAEtCF,eAAeH,gBAAiB,CAC5BM,UAAW,iBACA,CACPC,MAAO,CACHC,YAAY,MAKxBL,eAAeL,SAAU,CACrBQ,UAAW,iBACA,CAEPG,eAAgB,GAChBC,gBAAiB,GACjBC,iBAAkB,GAElBC,oBAAoB,EACpBC,mBAAoB,GACpBC,mBAAoB,CAEhBC,WAAW,EACXC,WAAW,EACXC,aAAa,EACbC,gBAAgB,MAK5Bf,eAAeF,QAAS,CACpBK,UAAW,iBACA,CAEPa,OAAQ,EACRC,SAAU,EACVC,eAAgB,aAWfC,eAAkBpB,QAAWA,OAAOE,QAAQmB,IAAIvB,qFAQzBE,QAAWoB,eAAepB,QAAQK,mCAQlCL,QAAWoB,eAAepB,QAAQsB,uBAQ9CtB,QAAWA,OAAOE,QAAQmB,IAAIzB,0BAQ/BI,QAAWA,OAAOE,QAAQmB,IAAItB"}

View File

@@ -0,0 +1,10 @@
define("tiny_mediacms/plugin",["exports","editor_tiny/loader","editor_tiny/utils","./common","./commands","./configuration","./options","./autoconvert"],(function(_exports,_loader,_utils,_common,Commands,Configuration,Options,_autoconvert){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}
/**
* Tiny Media plugin for Moodle.
*
* @module tiny_mediacms/plugin
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,Commands=_interopRequireWildcard(Commands),Configuration=_interopRequireWildcard(Configuration),Options=_interopRequireWildcard(Options);const isMediaCMSUrl=url=>{if(!url)return!1;try{const urlObj=new URL(url);return("/embed"===urlObj.pathname||"/view"===urlObj.pathname)&&urlObj.searchParams.has("m")}catch(e){return!1}},MEDIACMS_URL_PATTERN=/(^|>|\s)(https?:\/\/[^\s<>"]+\/(?:embed|view)\?m=[^\s<>"]+)(<|\s|$)/g;var _default=new Promise((async resolve=>{const[tinyMCE,setupCommands,pluginMetadata]=await Promise.all([(0,_loader.getTinyMCE)(),Commands.getSetup(),(0,_utils.getPluginMetadata)(_common.component,_common.pluginName)]);tinyMCE.PluginManager.add("".concat(_common.component,"/plugin"),(editor=>(Options.register(editor),setupCommands(editor),(0,_autoconvert.setupAutoConvert)(editor),editor.on("BeforeSetContent",(e=>{e.content&&"string"==typeof e.content&&(e.content=e.content.replace(MEDIACMS_URL_PATTERN,((match,before,url,after)=>isMediaCMSUrl(url)?before+(url=>{let embedUrl=url;try{const urlObj=new URL(url);"/view"===urlObj.pathname&&(urlObj.pathname="/embed",embedUrl=urlObj.toString())}catch(e){}return'<iframe src="'.concat(embedUrl,'" ')+'style="width: 100%; aspect-ratio: 16 / 9; display: block; border: 0;" allowfullscreen="allowfullscreen"></iframe>'})(url)+after:match)))})),editor.on("GetContent",(e=>{if("html"===e.format){const tempDiv=document.createElement("div");tempDiv.innerHTML=e.content,tempDiv.querySelectorAll(".tiny-mediacms-edit-btn").forEach((btn=>btn.remove())),tempDiv.querySelectorAll("iframe").forEach((iframe=>{const src=iframe.getAttribute("src");if(isMediaCMSUrl(src)){const wrapper=iframe.closest(".tiny-mediacms-iframe-wrapper")||iframe.closest(".tiny-iframe-responsive"),urlText=document.createTextNode(src),p=document.createElement("p");p.appendChild(urlText),wrapper?(wrapper.parentNode.insertBefore(p,wrapper),wrapper.remove()):(iframe.parentNode.insertBefore(p,iframe),iframe.remove())}})),tempDiv.querySelectorAll(".tiny-mediacms-iframe-wrapper").forEach((wrapper=>{const iframe=wrapper.querySelector("iframe");iframe&&wrapper.parentNode.insertBefore(iframe,wrapper),wrapper.remove()})),tempDiv.querySelectorAll(".tiny-iframe-responsive").forEach((wrapper=>{const iframe=wrapper.querySelector("iframe");iframe&&wrapper.parentNode.insertBefore(iframe,wrapper),wrapper.remove()})),e.content=tempDiv.innerHTML}})),pluginMetadata))),resolve(["".concat(_common.component,"/plugin"),Configuration])}));return _exports.default=_default,_exports.default}));
//# sourceMappingURL=plugin.min.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
define("tiny_mediacms/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={IMAGE:{actions:{submit:".tiny_imagecms_urlentrysubmit",imageBrowser:".openimagecmsbrowser",addUrl:".tiny_imagecms_addurl",deleteImage:".tiny_imagecms_deleteicon"},elements:{form:"form.tiny_imagecms_form",alignSettings:".tiny_imagecms_button",alt:".tiny_imagecms_altentry",altWarning:".tiny_imagecms_altwarning",height:".tiny_imagecms_heightentry",width:".tiny_imagecms_widthentry",url:".tiny_imagecms_urlentry",urlWarning:".tiny_imagecms_urlwarning",size:".tiny_imagecms_size",presentation:".tiny_imagecms_presentation",constrain:".tiny_imagecms_constrain",customStyle:".tiny_imagecms_customstyle",preview:".tiny_imagecms_preview",previewBox:".tiny_imagecms_preview_box",loaderIcon:".tiny_imagecms_loader",loaderIconContainer:".tiny_imagecms_loader_container",insertImage:".tiny_imagecms_insert_image",modalFooter:".modal-footer",dropzoneContainer:".tiny_imagecms_dropzone_container",fileInput:"#tiny_imagecms_fileinput",fileNameLabel:".tiny_imagecms_filename",sizeOriginal:".tiny_imagecms_sizeoriginal",sizeCustom:".tiny_imagecms_sizecustom",properties:".tiny_imagecms_properties"},styles:{responsive:"img-fluid"}},EMBED:{actions:{submit:".tiny_mediacms_submit",mediaBrowser:".openmediacmsbrowser"},elements:{form:"form.tiny_mediacms_form",source:".tiny_mediacms_source",track:".tiny_mediacms_track",mediaSource:".tiny_mediacms_media_source",linkSource:".tiny_mediacms_link_source",linkSize:".tiny_mediacms_link_size",posterSource:".tiny_mediacms_poster_source",posterSize:".tiny_mediacms_poster_size",displayOptions:".tiny_mediacms_display_options",name:".tiny_mediacms_name_entry",title:".tiny_mediacms_title_entry",url:".tiny_mediacms_url_entry",width:".tiny_mediacms_width_entry",height:".tiny_mediacms_height_entry",trackSource:".tiny_mediacms_track_source",trackKind:".tiny_mediacms_track_kind_entry",trackLabel:".tiny_mediacms_track_label_entry",trackLang:".tiny_mediacms_track_lang_entry",trackDefault:".tiny_mediacms_track_default",mediaControl:".tiny_mediacms_controls",mediaAutoplay:".tiny_mediacms_autoplay",mediaMute:".tiny_mediacms_mute",mediaLoop:".tiny_mediacms_loop",advancedSettings:".tiny_mediacms_advancedsettings",linkTab:'li[data-medium-type="link"]',videoTab:'li[data-medium-type="video"]',audioTab:'li[data-medium-type="audio"]',linkPane:'.tab-pane[data-medium-type="link"]',videoPane:'.tab-pane[data-medium-type="video"]',audioPane:'.tab-pane[data-medium-type="audio"]',trackSubtitlesTab:'li[data-track-kind="subtitles"]',trackCaptionsTab:'li[data-track-kind="captions"]',trackDescriptionsTab:'li[data-track-kind="descriptions"]',trackChaptersTab:'li[data-track-kind="chapters"]',trackMetadataTab:'li[data-track-kind="metadata"]',trackSubtitlesPane:'.tab-pane[data-track-kind="subtitles"]',trackCaptionsPane:'.tab-pane[data-track-kind="captions"]',trackDescriptionsPane:'.tab-pane[data-track-kind="descriptions"]',trackChaptersPane:'.tab-pane[data-track-kind="chapters"]',trackMetadataPane:'.tab-pane[data-track-kind="metadata"]'},mediaTypes:{link:"LINK",video:"VIDEO",audio:"AUDIO"},trackKinds:{subtitles:"SUBTITLES",captions:"CAPTIONS",descriptions:"DESCRIPTIONS",chapters:"CHAPTERS",metadata:"METADATA"}},IFRAME:{actions:{remove:'[data-action="remove"]'},elements:{form:"form.tiny_iframecms_form",url:".tiny_iframecms_url",urlWarning:".tiny_iframecms_url_warning",showTitle:".tiny_iframecms_showtitle",linkTitle:".tiny_iframecms_linktitle",showRelated:".tiny_iframecms_showrelated",showUserAvatar:".tiny_iframecms_showuseravatar",responsive:".tiny_iframecms_responsive",startAt:".tiny_iframecms_startat",startAtEnabled:".tiny_iframecms_startat_enabled",aspectRatio:".tiny_iframecms_aspectratio",width:".tiny_iframecms_width",height:".tiny_iframecms_height",preview:".tiny_iframecms_preview",previewContainer:".tiny_iframecms_preview_container",tabs:".tiny_iframecms_tabs",tabUrlBtn:".tiny_iframecms_tab_url_btn",tabIframeLibraryBtn:".tiny_iframecms_tab_iframe_library_btn",paneUrl:".tiny_iframecms_pane_url",paneIframeLibrary:".tiny_iframecms_pane_iframe_library",iframeLibraryContainer:".tiny_iframecms_iframe_library_container",iframeLibraryPlaceholder:".tiny_iframecms_iframe_library_placeholder",iframeLibraryLoading:".tiny_iframecms_iframe_library_loading",iframeLibraryFrame:".tiny_iframecms_iframe_library_frame"},aspectRatios:{"16:9":{width:560,height:315},"4:3":{width:560,height:420},"1:1":{width:400,height:400},custom:null}}},_exports.default}));
//# sourceMappingURL=selectors.min.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,10 @@
define("tiny_mediacms/usedfiles",["exports","core/templates","core/config"],(function(_exports,Templates,_config){var obj;function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,Templates=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}
/**
* Tiny Media Manager usedfiles.
*
* @module tiny_mediacms/usedfiles
* @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/(Templates),_config=(obj=_config)&&obj.__esModule?obj:{default:obj};class UsedFileManager{constructor(files,userContext,itemId,elementId){this.files=files,this.userContext=userContext,this.itemId=itemId,this.elementId=elementId}getElementId(){return this.elementId}getUsedFiles(){const editor=window.parent.tinymce.EditorManager.get(this.getElementId());if(!editor)return window.console.error("Editor not found for ".concat(this.getElementId())),[];const content=editor.getContent(),baseUrl="".concat(_config.default.wwwroot,"/draftfile.php/").concat(this.userContext,"/user/draft/").concat(this.itemId,"/"),pattern=new RegExp("[\"']"+baseUrl.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&")+"(?<filename>.+?)[\\?\"']","gm");return[...content.matchAll(pattern)].map((match=>decodeURIComponent(match.groups.filename)))}findUnusedFiles(usedFiles){return Object.entries(this.files).filter((_ref=>{let[filename]=_ref;return!usedFiles.includes(filename)})).map((_ref2=>{let[filename]=_ref2;return filename}))}findMissingFiles(usedFiles){return usedFiles.filter((filename=>!this.files.hasOwnProperty(filename)))}updateFiles(){const form=document.querySelector("form"),usedFiles=this.getUsedFiles(),unusedFiles=this.findUnusedFiles(usedFiles),missingFiles=this.findMissingFiles(usedFiles);return form.querySelectorAll('input[type=checkbox][name^="deletefile"]').forEach((checkbox=>{unusedFiles.includes(checkbox.dataset.filename)||checkbox.closest(".fitem").remove()})),form.classList.toggle("has-missing-files",!!missingFiles.length),form.classList.toggle("has-unused-files",!!unusedFiles.length),Templates.renderForPromise("tiny_mediacms/missingfiles",{missingFiles:missingFiles}).then((_ref3=>{let{html:html,js:js}=_ref3;Templates.replaceNodeContents(form.querySelector(".missing-files"),html,js)}))}}_exports.init=(files,usercontext,itemid,elementid)=>{const manager=new UsedFileManager(files,usercontext,itemid,elementid);return manager.updateFiles(),manager}}));
//# sourceMappingURL=usedfiles.min.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,264 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Tiny MediaCMS Auto-convert module.
*
* This module automatically converts pasted MediaCMS URLs into embedded videos.
* When a user pastes a MediaCMS video URL (e.g., https://deic.mediacms.io/view?m=JpBd1Zvdl),
* it will be automatically converted to an iframe embed.
*
* @module tiny_mediacms/autoconvert
* @copyright 2024
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {getData} from './options';
/**
* Regular expression patterns for MediaCMS URLs.
* Matches URLs like:
* - https://deic.mediacms.io/view?m=JpBd1Zvdl
* - https://example.mediacms.io/view?m=VIDEO_ID
* - Custom domains configured in the plugin
*/
const MEDIACMS_VIEW_URL_PATTERN = /^(https?:\/\/[^\/]+)\/view\?m=([a-zA-Z0-9_-]+)$/;
/**
* Check if a string is a valid MediaCMS view URL.
*
* @param {string} text - The text to check
* @returns {Object|null} - Parsed URL info or null if not a valid MediaCMS URL
*/
const parseMediaCMSUrl = (text) => {
if (!text || typeof text !== 'string') {
return null;
}
const trimmed = text.trim();
// Check for MediaCMS view URL pattern
const match = trimmed.match(MEDIACMS_VIEW_URL_PATTERN);
if (match) {
return {
baseUrl: match[1],
videoId: match[2],
originalUrl: trimmed,
};
}
return null;
};
/**
* Check if the pasted URL's domain is allowed based on configuration.
*
* @param {Object} parsed - Parsed URL info
* @param {Object} config - Plugin configuration
* @returns {boolean} - True if the domain is allowed
*/
const isDomainAllowed = (parsed, config) => {
// If no specific base URL is configured, allow all MediaCMS domains
const configuredBaseUrl = config.autoConvertBaseUrl || config.mediacmsBaseUrl;
if (!configuredBaseUrl) {
return true;
}
// Check if the URL's base matches the configured base URL
try {
const configuredUrl = new URL(configuredBaseUrl);
const pastedUrl = new URL(parsed.baseUrl);
return configuredUrl.host === pastedUrl.host;
} catch (e) {
// If URL parsing fails, allow the conversion
return true;
}
};
/**
* Generate the iframe embed HTML for a MediaCMS video.
*
* @param {Object} parsed - Parsed URL info
* @param {Object} options - Embed options
* @returns {string} - The iframe HTML
*/
const generateEmbedHtml = (parsed, options = {}) => {
// Build the embed URL with default options
const embedUrl = new URL(`${parsed.baseUrl}/embed`);
embedUrl.searchParams.set('m', parsed.videoId);
// Apply default options (all enabled by default for best user experience)
embedUrl.searchParams.set('showTitle', options.showTitle !== false ? '1' : '0');
embedUrl.searchParams.set('showRelated', options.showRelated !== false ? '1' : '0');
embedUrl.searchParams.set('showUserAvatar', options.showUserAvatar !== false ? '1' : '0');
embedUrl.searchParams.set('linkTitle', options.linkTitle !== false ? '1' : '0');
// Generate responsive iframe HTML matching the template output format.
// Uses aspect-ratio CSS for responsive sizing (16:9 default).
// The wrapper will be added by editor for UI (edit button), then stripped on save.
const html = `<iframe src="${embedUrl.toString()}" ` +
`style="width: 100%; aspect-ratio: 16 / 9; display: block; border: 0;" ` +
`allowfullscreen="allowfullscreen"></iframe>`;
return html;
};
/**
* Set up auto-conversion for the editor.
* This registers event handlers to detect pasted MediaCMS URLs.
*
* @param {TinyMCE} editor - The TinyMCE editor instance
*/
export const setupAutoConvert = (editor) => {
const config = getData(editor) || {};
// Check if auto-convert is enabled (default: true)
if (config.autoConvertEnabled === false) {
return;
}
// Handle paste events
editor.on('paste', (e) => {
handlePasteEvent(editor, e, config);
});
// Also handle input events for drag-and-drop text or keyboard paste
editor.on('input', (e) => {
handleInputEvent(editor, e, config);
});
};
/**
* Handle paste events to detect and convert MediaCMS URLs.
*
* @param {TinyMCE} editor - The TinyMCE editor instance
* @param {Event} e - The paste event
* @param {Object} config - Plugin configuration
*/
const handlePasteEvent = (editor, e, config) => {
// Get pasted text from clipboard
const clipboardData = e.clipboardData || window.clipboardData;
if (!clipboardData) {
return;
}
// Try to get plain text first
const text = clipboardData.getData('text/plain') || clipboardData.getData('text');
if (!text) {
return;
}
// Check if it's a MediaCMS URL
const parsed = parseMediaCMSUrl(text);
if (!parsed) {
return;
}
// Check if domain is allowed
if (!isDomainAllowed(parsed, config)) {
return;
}
// Prevent default paste behavior
e.preventDefault();
e.stopPropagation();
// Generate and insert the embed HTML
const embedHtml = generateEmbedHtml(parsed, config.autoConvertOptions || {});
// Use a slight delay to ensure the editor is ready
setTimeout(() => {
editor.insertContent(embedHtml);
// Move cursor after the inserted content
editor.selection.collapse(false);
}, 0);
};
/**
* Handle input events to catch URLs that might have been pasted without triggering paste event.
* This is a fallback for certain browsers/scenarios.
*
* @param {TinyMCE} editor - The TinyMCE editor instance
* @param {Event} e - The input event
* @param {Object} config - Plugin configuration
*/
const handleInputEvent = (editor, e, config) => {
// Only process inputType 'insertFromPaste' if paste event didn't catch it
if (e.inputType !== 'insertFromPaste' && e.inputType !== 'insertText') {
return;
}
// Get the current node and check if it contains just a URL
const node = editor.selection.getNode();
if (!node || node.nodeName !== 'P') {
return;
}
// Check if the paragraph contains only a MediaCMS URL
const text = node.textContent || '';
const parsed = parseMediaCMSUrl(text);
if (!parsed || !isDomainAllowed(parsed, config)) {
return;
}
// Don't convert if there's other content in the paragraph
const trimmedHtml = node.innerHTML.trim();
if (trimmedHtml !== text.trim() && !trimmedHtml.startsWith(text.trim())) {
return;
}
// Generate the embed HTML
const embedHtml = generateEmbedHtml(parsed, config.autoConvertOptions || {});
// Replace the paragraph content with the embed
// Use a slight delay to let the input event complete
setTimeout(() => {
// Re-check that the node still contains the URL (user might have typed more)
const currentText = node.textContent || '';
const currentParsed = parseMediaCMSUrl(currentText);
if (currentParsed && currentParsed.originalUrl === parsed.originalUrl) {
// Select and replace the entire node
editor.selection.select(node);
editor.insertContent(embedHtml);
}
}, 100);
};
/**
* Check if a text is a MediaCMS URL (public helper).
*
* @param {string} text - The text to check
* @returns {boolean} - True if it's a MediaCMS URL
*/
export const isMediaCMSUrl = (text) => {
return parseMediaCMSUrl(text) !== null;
};
/**
* Convert a MediaCMS URL to embed HTML (public helper).
*
* @param {string} url - The MediaCMS URL
* @param {Object} options - Embed options
* @returns {string|null} - The embed HTML or null if not a valid URL
*/
export const convertToEmbed = (url, options = {}) => {
const parsed = parseMediaCMSUrl(url);
if (!parsed) {
return null;
}
return generateEmbedHtml(parsed, options);
};

View File

@@ -0,0 +1,282 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Tiny Media commands.
*
* @module tiny_mediacms/commands
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {getStrings} from 'core/str';
import {
component,
iframeButtonName,
iframeMenuItemName,
iframeIcon,
} from './common';
import IframeEmbed from './iframeembed';
import {getButtonImage} from 'editor_tiny/utils';
const isIframe = (node) => node.nodeName.toLowerCase() === 'iframe' ||
(node.classList && node.classList.contains('tiny-iframe-responsive')) ||
(node.classList && node.classList.contains('tiny-mediacms-iframe-wrapper'));
/**
* Wrap iframes with overlay containers that allow hover detection.
* Since iframes capture mouse events, we add an invisible overlay on top
* that shows the edit button on hover.
*
* @param {TinyMCE} editor - The editor instance
* @param {Function} handleIframeAction - The action to perform when clicking the button
*/
const setupIframeOverlays = (editor, handleIframeAction) => {
/**
* Process all iframes in the editor and add overlay wrappers.
*/
const processIframes = () => {
const editorBody = editor.getBody();
if (!editorBody) {
return;
}
const iframes = editorBody.querySelectorAll('iframe');
iframes.forEach((iframe) => {
// Skip if already wrapped
if (iframe.parentElement?.classList.contains('tiny-mediacms-iframe-wrapper')) {
return;
}
// Skip TinyMCE internal iframes
if (iframe.hasAttribute('data-mce-object') || iframe.hasAttribute('data-mce-placeholder')) {
return;
}
// Create wrapper div
const wrapper = editor.getDoc().createElement('div');
wrapper.className = 'tiny-mediacms-iframe-wrapper';
wrapper.setAttribute('contenteditable', 'false');
// Create edit button (positioned inside wrapper, over the iframe)
const editBtn = editor.getDoc().createElement('button');
editBtn.className = 'tiny-mediacms-edit-btn';
editBtn.setAttribute('type', 'button');
editBtn.setAttribute('title', 'Edit video embed options');
// Use clean inline SVG to avoid TinyMCE wrapper issues
editBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">' +
'<circle cx="50" cy="50" r="48" fill="#2EAF5A"/>' +
'<polygon points="38,28 38,72 75,50" fill="#FFFFFF"/>' +
'</svg>';
// Wrap the iframe: insert wrapper, move iframe into it, add button
iframe.parentNode.insertBefore(wrapper, iframe);
wrapper.appendChild(iframe);
wrapper.appendChild(editBtn);
});
};
/**
* Add CSS styles for hover effects to the editor's document.
*/
const addStyles = () => {
const editorDoc = editor.getDoc();
if (!editorDoc) {
return;
}
// Check if styles already added
if (editorDoc.getElementById('tiny-mediacms-overlay-styles')) {
return;
}
const style = editorDoc.createElement('style');
style.id = 'tiny-mediacms-overlay-styles';
style.textContent = `
.tiny-mediacms-iframe-wrapper {
display: inline-block;
position: relative;
line-height: 0;
vertical-align: top;
}
.tiny-mediacms-iframe-wrapper iframe {
display: block;
}
.tiny-mediacms-edit-btn {
position: absolute;
top: 48px;
left: 6px;
width: 28px;
height: 28px;
background: #ffffff;
border: none;
border-radius: 50%;
cursor: pointer;
z-index: 10;
padding: 0;
margin: 0;
box-shadow: 0 2px 6px rgba(0,0,0,0.35);
transition: transform 0.15s, box-shadow 0.15s;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.tiny-mediacms-edit-btn:hover {
transform: scale(1.15);
box-shadow: 0 3px 10px rgba(0,0,0,0.45);
}
.tiny-mediacms-edit-btn svg {
width: 18px !important;
height: 18px !important;
display: block !important;
}
`;
editorDoc.head.appendChild(style);
};
/**
* Handle click on the edit button.
*
* @param {Event} e - The click event
*/
const handleOverlayClick = (e) => {
const target = e.target;
// Check if clicked on edit button or its child (svg/path)
const editBtn = target.closest('.tiny-mediacms-edit-btn');
if (!editBtn) {
return;
}
e.preventDefault();
e.stopPropagation();
// Find the associated wrapper and iframe
const wrapper = editBtn.closest('.tiny-mediacms-iframe-wrapper');
if (!wrapper) {
return;
}
const iframe = wrapper.querySelector('iframe');
if (!iframe) {
return;
}
// Select the wrapper so TinyMCE knows which element is selected
editor.selection.select(wrapper);
// Open the edit dialog
handleIframeAction();
};
// Setup on editor init
editor.on('init', () => {
addStyles();
processIframes();
// Handle clicks on the overlay
editor.getBody().addEventListener('click', handleOverlayClick);
});
// Re-process when content changes
editor.on('SetContent', () => {
processIframes();
});
// Re-process when content is pasted
editor.on('PastePostProcess', () => {
setTimeout(processIframes, 100);
});
// Re-process after undo/redo
editor.on('Undo Redo', () => {
processIframes();
});
// Re-process on any content change (covers modal updates)
editor.on('Change', () => {
setTimeout(processIframes, 50);
});
// Re-process when node changes (selection changes)
editor.on('NodeChange', () => {
processIframes();
});
};
const registerIframeCommand = (editor, iframeButtonText, iframeButtonImage) => {
const handleIframeAction = () => {
const iframeEmbed = new IframeEmbed(editor);
iframeEmbed.displayDialogue();
};
// Register the iframe icon
editor.ui.registry.addIcon(iframeIcon, iframeButtonImage.html);
// Register the Menu Button as a toggle.
// This means that when highlighted over an existing iframe element it will show as toggled on.
editor.ui.registry.addToggleButton(iframeButtonName, {
icon: iframeIcon,
tooltip: iframeButtonText,
onAction: handleIframeAction,
onSetup: api => {
return editor.selection.selectorChangedWithUnbind(
'iframe:not([data-mce-object]):not([data-mce-placeholder]),.tiny-iframe-responsive,.tiny-mediacms-iframe-wrapper',
api.setActive
).unbind;
}
});
editor.ui.registry.addMenuItem(iframeMenuItemName, {
icon: iframeIcon,
text: iframeButtonText,
onAction: handleIframeAction,
});
editor.ui.registry.addContextToolbar(iframeButtonName, {
predicate: isIframe,
items: iframeButtonName,
position: 'node',
scope: 'node'
});
editor.ui.registry.addContextMenu(iframeButtonName, {
update: isIframe,
});
// Setup iframe overlays with edit button on hover
setupIframeOverlays(editor, handleIframeAction);
};
export const getSetup = async() => {
const [
iframeButtonText,
] = await getStrings([
'iframebuttontitle',
].map((key) => ({key, component})));
const [
iframeButtonImage,
] = await Promise.all([
getButtonImage('icon', component),
]);
// Note: The function returned here must be synchronous and cannot use promises.
// All promises must be resolved prior to returning the function.
return (editor) => {
registerIframeCommand(editor, iframeButtonText, iframeButtonImage);
};
};

View File

@@ -0,0 +1,30 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Tiny Media common values.
*
* @module tiny_mediacms/common
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export default {
pluginName: 'tiny_mediacms/plugin',
component: 'tiny_mediacms',
iframeButtonName: 'tiny_mediacms_iframe',
iframeMenuItemName: 'tiny_mediacms_iframe',
iframeIcon: 'tiny_mediacms_iframe',
};

View File

@@ -0,0 +1,60 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Tiny Media configuration.
*
* @module tiny_mediacms/configuration
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {
iframeButtonName,
iframeMenuItemName,
} from './common';
import {
addContextmenuItem,
} from 'editor_tiny/utils';
const configureMenu = (menu) => {
// Add the Iframe Embed to the insert menu.
menu.insert.items = `${iframeMenuItemName} ${menu.insert.items}`;
return menu;
};
const configureToolbar = (toolbar) => {
// The toolbar contains an array of named sections.
// The Moodle integration ensures that there is a section called 'content'.
return toolbar.map((section) => {
if (section.name === 'content') {
// Insert the iframe button at the start of it.
section.items.unshift(iframeButtonName);
}
return section;
});
};
export const configure = (instanceConfig) => {
// Update the instance configuration to add the Iframe Embed menu option to the menus and toolbars.
return {
contextmenu: addContextmenuItem(instanceConfig.contextmenu, iframeButtonName),
menu: configureMenu(instanceConfig.menu),
toolbar: configureToolbar(instanceConfig.toolbar),
};
};

View File

@@ -0,0 +1,467 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Tiny Media plugin Embed class for Moodle.
*
* @module tiny_mediacms/embed
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Templates from 'core/templates';
import {
getString,
getStrings,
} from 'core/str';
import * as ModalEvents from 'core/modal_events';
import {displayFilepicker} from 'editor_tiny/utils';
import {getCurrentLanguage, getMoodleLang} from 'editor_tiny/options';
import {component} from "./common";
import EmbedModal from './embedmodal';
import Selectors from './selectors';
import {getEmbedPermissions} from './options';
import {getFilePicker} from 'editor_tiny/options';
export default class MediaEmbed {
editor = null;
canShowFilePicker = false;
canShowFilePickerPoster = false;
canShowFilePickerTrack = false;
/**
* @property {Object} The names of the alignment options.
*/
helpStrings = null;
/**
* @property {boolean} Indicate that the user is updating the media or not.
*/
isUpdating = false;
/**
* @property {Object} The currently selected media.
*/
selectedMedia = null;
constructor(editor) {
const permissions = getEmbedPermissions(editor);
// Indicates whether the file picker can be shown.
this.canShowFilePicker = permissions.filepicker && (typeof getFilePicker(editor, 'media') !== 'undefined');
this.canShowFilePickerPoster = permissions.filepicker && (typeof getFilePicker(editor, 'image') !== 'undefined');
this.canShowFilePickerTrack = permissions.filepicker && (typeof getFilePicker(editor, 'subtitle') !== 'undefined');
this.editor = editor;
}
async getHelpStrings() {
if (!this.helpStrings) {
const [addSource, tracks, subtitles, captions, descriptions, chapters, metadata] = await getStrings([
'addsource_help',
'tracks_help',
'subtitles_help',
'captions_help',
'descriptions_help',
'chapters_help',
'metadata_help',
].map((key) => ({
key,
component,
})));
this.helpStrings = {addSource, tracks, subtitles, captions, descriptions, chapters, metadata};
}
return this.helpStrings;
}
async getTemplateContext(data) {
const languages = this.prepareMoodleLang();
const helpIcons = Array.from(Object.entries(await this.getHelpStrings())).forEach(([key, text]) => {
data[`${key.toLowerCase()}helpicon`] = {text};
});
return Object.assign({}, {
elementid: this.editor.getElement().id,
showfilepicker: this.canShowFilePicker,
showfilepickerposter: this.canShowFilePickerPoster,
showfilepickertrack: this.canShowFilePickerTrack,
langsinstalled: languages.installed,
langsavailable: languages.available,
link: true,
video: false,
audio: false,
isupdating: this.isUpdating,
}, data, helpIcons);
}
async displayDialogue() {
this.selectedMedia = this.getSelectedMedia();
const data = Object.assign({}, this.getCurrentEmbedData());
this.isUpdating = Object.keys(data).length !== 0;
this.currentModal = await EmbedModal.create({
title: getString('createmedia', 'tiny_mediacms'),
templateContext: await this.getTemplateContext(data),
});
await this.registerEventListeners(this.currentModal);
}
getCurrentEmbedData() {
const properties = this.getMediumProperties();
if (!properties) {
return {};
}
const processedProperties = {};
processedProperties[properties.type.toLowerCase()] = properties;
processedProperties.link = false;
return processedProperties;
}
getSelectedMedia() {
const mediaElm = this.editor.selection.getNode();
if (!mediaElm) {
return null;
}
if (mediaElm.nodeName.toLowerCase() === 'video' || mediaElm.nodeName.toLowerCase() === 'audio') {
return mediaElm;
}
if (mediaElm.querySelector('video')) {
return mediaElm.querySelector('video');
}
if (mediaElm.querySelector('audio')) {
return mediaElm.querySelector('audio');
}
return null;
}
getMediumProperties() {
const boolAttr = (elem, attr) => {
// As explained in MDL-64175, some OS (like Ubuntu), are removing the value for these attributes.
// So in order to check if attr="true", we need to check if the attribute exists and if the value is empty or true.
return (elem.hasAttribute(attr) && (elem.getAttribute(attr) || elem.getAttribute(attr) === ''));
};
const tracks = {
subtitles: [],
captions: [],
descriptions: [],
chapters: [],
metadata: []
};
const sources = [];
const medium = this.selectedMedia;
if (!medium) {
return null;
}
medium.querySelectorAll('track').forEach((track) => {
tracks[track.getAttribute('kind')].push({
src: track.getAttribute('src'),
srclang: track.getAttribute('srclang'),
label: track.getAttribute('label'),
defaultTrack: boolAttr(track, 'default')
});
});
medium.querySelectorAll('source').forEach((source) => {
sources.push(source.src);
});
return {
type: medium.nodeName.toLowerCase() === 'video' ? Selectors.EMBED.mediaTypes.video : Selectors.EMBED.mediaTypes.audio,
sources,
poster: medium.getAttribute('poster'),
title: medium.getAttribute('title'),
width: medium.getAttribute('width'),
height: medium.getAttribute('height'),
autoplay: boolAttr(medium, 'autoplay'),
loop: boolAttr(medium, 'loop'),
muted: boolAttr(medium, 'muted'),
controls: boolAttr(medium, 'controls'),
tracks,
};
}
prepareMoodleLang() {
const moodleLangs = getMoodleLang(this.editor);
const currentLanguage = getCurrentLanguage(this.editor);
const installed = Object.entries(moodleLangs.installed).map(([lang, code]) => ({
lang,
code,
"default": lang === currentLanguage,
}));
const available = Object.entries(moodleLangs.available).map(([lang, code]) => ({
lang,
code,
"default": lang === currentLanguage,
}));
return {
installed,
available,
};
}
getMoodleLangObj(subtitleLang) {
const {available} = getMoodleLang(this.editor);
if (available[subtitleLang]) {
return {
lang: subtitleLang,
code: available[subtitleLang],
};
}
return null;
}
filePickerCallback(params, element, fpType) {
if (params.url !== '') {
const tabPane = element.closest('.tab-pane');
element.closest(Selectors.EMBED.elements.source).querySelector(Selectors.EMBED.elements.url).value = params.url;
if (tabPane.id === this.editor.getElement().id + '_' + Selectors.EMBED.mediaTypes.link.toLowerCase()) {
tabPane.querySelector(Selectors.EMBED.elements.name).value = params.file;
}
if (fpType === 'subtitle') {
// If the file is subtitle file. We need to match the language and label for that file.
const subtitleLang = params.file.split('.vtt')[0].split('-').slice(-1)[0];
const langObj = this.getMoodleLangObj(subtitleLang);
if (langObj) {
const track = element.closest(Selectors.EMBED.elements.track);
track.querySelector(Selectors.EMBED.elements.trackLabel).value = langObj.lang.trim();
track.querySelector(Selectors.EMBED.elements.trackLang).value = langObj.code;
}
}
}
}
addMediaSourceComponent(element, callback) {
const sourceElement = element.closest(Selectors.EMBED.elements.source + Selectors.EMBED.elements.mediaSource);
const clone = sourceElement.cloneNode(true);
sourceElement.querySelector('.removecomponent-wrapper').classList.remove('hidden');
sourceElement.querySelector('.addcomponent-wrapper').classList.add('hidden');
sourceElement.parentNode.insertBefore(clone, sourceElement.nextSibling);
if (callback) {
callback(clone);
}
}
removeMediaSourceComponent(element) {
const sourceElement = element.closest(Selectors.EMBED.elements.source + Selectors.EMBED.elements.mediaSource);
sourceElement.remove();
}
addTrackComponent(element, callback) {
const trackElement = element.closest(Selectors.EMBED.elements.track);
const clone = trackElement.cloneNode(true);
trackElement.querySelector('.removecomponent-wrapper').classList.remove('hidden');
trackElement.querySelector('.addcomponent-wrapper').classList.add('hidden');
trackElement.parentNode.insertBefore(clone, trackElement.nextSibling);
if (callback) {
callback(clone);
}
}
removeTrackComponent(element) {
const sourceElement = element.closest(Selectors.EMBED.elements.track);
sourceElement.remove();
}
getMediumTypeFromTabPane(tabPane) {
return tabPane.getAttribute('data-medium-type');
}
getTrackTypeFromTabPane(tabPane) {
return tabPane.getAttribute('data-track-kind');
}
getMediaHTML(form) {
const mediumType = this.getMediumTypeFromTabPane(form.querySelector('.root.tab-content > .tab-pane.active'));
const tabContent = form.querySelector(Selectors.EMBED.elements[mediumType.toLowerCase() + 'Pane']);
return this['getMediaHTML' + mediumType[0].toUpperCase() + mediumType.substr(1)](tabContent);
}
getMediaHTMLLink(tab) {
const context = {
url: tab.querySelector(Selectors.EMBED.elements.url).value,
name: tab.querySelector(Selectors.EMBED.elements.name).value || false
};
return context.url ? Templates.renderForPromise('tiny_mediacms/embed_media_link', context) : '';
}
getMediaHTMLVideo(tab) {
const context = this.getContextForMediaHTML(tab);
context.width = tab.querySelector(Selectors.EMBED.elements.width).value || false;
context.height = tab.querySelector(Selectors.EMBED.elements.height).value || false;
context.poster = tab.querySelector(
`${Selectors.EMBED.elements.posterSource} ${Selectors.EMBED.elements.url}`
).value || false;
return context.sources.length ? Templates.renderForPromise('tiny_mediacms/embed_media_video', context) : '';
}
getMediaHTMLAudio(tab) {
const context = this.getContextForMediaHTML(tab);
return context.sources.length ? Templates.renderForPromise('tiny_mediacms/embed_media_audio', context) : '';
}
getContextForMediaHTML(tab) {
const tracks = Array.from(tab.querySelectorAll(Selectors.EMBED.elements.track)).map(track => ({
track: track.querySelector(Selectors.EMBED.elements.trackSource + ' ' + Selectors.EMBED.elements.url).value,
kind: this.getTrackTypeFromTabPane(track.closest('.tab-pane')),
label: track.querySelector(Selectors.EMBED.elements.trackLabel).value ||
track.querySelector(Selectors.EMBED.elements.trackLang).value,
srclang: track.querySelector(Selectors.EMBED.elements.trackLang).value,
defaultTrack: track.querySelector(Selectors.EMBED.elements.trackDefault).checked ? "true" : null
})).filter((track) => !!track.track);
const sources = Array.from(tab.querySelectorAll(Selectors.EMBED.elements.mediaSource + ' '
+ Selectors.EMBED.elements.url))
.filter((source) => !!source.value)
.map((source) => source.value);
return {
sources,
description: tab.querySelector(Selectors.EMBED.elements.mediaSource + ' '
+ Selectors.EMBED.elements.url).value || false,
tracks,
showControls: tab.querySelector(Selectors.EMBED.elements.mediaControl).checked,
autoplay: tab.querySelector(Selectors.EMBED.elements.mediaAutoplay).checked,
muted: tab.querySelector(Selectors.EMBED.elements.mediaMute).checked,
loop: tab.querySelector(Selectors.EMBED.elements.mediaLoop).checked,
title: tab.querySelector(Selectors.EMBED.elements.title).value || false
};
}
getFilepickerTypeFromElement(element) {
if (element.closest(Selectors.EMBED.elements.posterSource)) {
return 'image';
}
if (element.closest(Selectors.EMBED.elements.trackSource)) {
return 'subtitle';
}
return 'media';
}
async clickHandler(e) {
const element = e.target;
const mediaBrowser = element.closest(Selectors.EMBED.actions.mediaBrowser);
if (mediaBrowser) {
e.preventDefault();
const fpType = this.getFilepickerTypeFromElement(element);
const params = await displayFilepicker(this.editor, fpType);
this.filePickerCallback(params, element, fpType);
}
const addComponentSourceAction = element.closest(Selectors.EMBED.elements.mediaSource + ' .addcomponent');
if (addComponentSourceAction) {
e.preventDefault();
this.addMediaSourceComponent(element);
}
const removeComponentSourceAction = element.closest(Selectors.EMBED.elements.mediaSource + ' .removecomponent');
if (removeComponentSourceAction) {
e.preventDefault();
this.removeMediaSourceComponent(element);
}
const addComponentTrackAction = element.closest(Selectors.EMBED.elements.track + ' .addcomponent');
if (addComponentTrackAction) {
e.preventDefault();
this.addTrackComponent(element);
}
const removeComponentTrackAction = element.closest(Selectors.EMBED.elements.track + ' .removecomponent');
if (removeComponentTrackAction) {
e.preventDefault();
this.removeTrackComponent(element);
}
// Only allow one track per tab to be selected as "default".
const trackDefaultAction = element.closest(Selectors.EMBED.elements.trackDefault);
if (trackDefaultAction && trackDefaultAction.checked) {
const getKind = (el) => this.getTrackTypeFromTabPane(el.parentElement.closest('.tab-pane'));
element.parentElement
.closest('.root.tab-content')
.querySelectorAll(Selectors.EMBED.elements.trackDefault)
.forEach((select) => {
if (select !== element && getKind(element) === getKind(select)) {
select.checked = false;
}
});
}
}
async handleDialogueSubmission(event, modal) {
const {html} = await this.getMediaHTML(modal.getRoot()[0]);
if (html) {
if (this.isUpdating) {
this.selectedMedia.outerHTML = html;
this.isUpdating = false;
} else {
this.editor.insertContent(html);
}
}
}
async registerEventListeners(modal) {
await modal.getBody();
const $root = modal.getRoot();
const root = $root[0];
if (this.canShowFilePicker || this.canShowFilePickerPoster || this.canShowFilePickerTrack) {
root.addEventListener('click', this.clickHandler.bind(this));
}
$root.on(ModalEvents.save, this.handleDialogueSubmission.bind(this));
$root.on(ModalEvents.hidden, () => {
this.currentModal.destroy();
});
$root.on(ModalEvents.shown, () => {
root.querySelectorAll(Selectors.EMBED.elements.trackLang).forEach((dropdown) => {
const defaultVal = dropdown.getAttribute('data-value');
if (defaultVal) {
dropdown.value = defaultVal;
}
});
});
}
}

View File

@@ -0,0 +1,47 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Embedded Media Management Modal for Tiny.
*
* @module tiny_mediacms/embedmodal
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Modal from 'core/modal';
import {component} from './common';
export default class EmbedModal extends Modal {
static TYPE = `${component}/modal`;
static TEMPLATE = `${component}/embed_media_modal`;
registerEventListeners() {
// Call the parent registration.
super.registerEventListeners();
// Register to close on save/cancel.
this.registerCloseOnSave();
this.registerCloseOnCancel();
}
configure(modalConfig) {
modalConfig.large = true;
modalConfig.removeOnClose = true;
modalConfig.show = true;
super.configure(modalConfig);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Iframe Embed Modal for Tiny Media2.
*
* @module tiny_mediacms/iframemodal
* @copyright 2024
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Modal from 'core/modal';
import {component} from './common';
export default class IframeModal extends Modal {
static TYPE = `${component}/iframemodal`;
static TEMPLATE = `${component}/iframe_embed_modal`;
registerEventListeners() {
// Call the parent registration.
super.registerEventListeners();
// Register to close on save/cancel.
this.registerCloseOnSave();
this.registerCloseOnCancel();
}
configure(modalConfig) {
modalConfig.large = true;
modalConfig.removeOnClose = true;
modalConfig.show = true;
super.configure(modalConfig);
}
}

View File

@@ -0,0 +1,273 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Tiny Media plugin Image class for Moodle.
*
* @module tiny_mediacms/image
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Selectors from './selectors';
import ImageModal from './imagemodal';
import {getImagePermissions} from './options';
import {getFilePicker} from 'editor_tiny/options';
import {ImageInsert} from 'tiny_mediacms/imageinsert';
import {ImageDetails} from 'tiny_mediacms/imagedetails';
import {prefetchStrings} from 'core/prefetch';
import {getString} from 'core/str';
import {
bodyImageInsert,
footerImageInsert,
bodyImageDetails,
footerImageDetails,
showElements,
hideElements,
isPercentageValue,
} from 'tiny_mediacms/imagehelpers';
prefetchStrings('tiny_mediacms', [
'imageurlrequired',
'sizecustom_help',
]);
export default class MediaImage {
canShowFilePicker = false;
editor = null;
currentModal = null;
/**
* @type {HTMLElement|null} The root element.
*/
root = null;
constructor(editor) {
const permissions = getImagePermissions(editor);
const options = getFilePicker(editor, 'image');
// Indicates whether the file picker can be shown.
this.canShowFilePicker = permissions.filepicker
&& (typeof options !== 'undefined')
&& Object.keys(options.repositories).length > 0;
// Indicates whether the drop zone area can be shown.
this.canShowDropZone = (typeof options !== 'undefined') &&
Object.values(options.repositories).some(repository => repository.type === 'upload');
this.editor = editor;
}
async displayDialogue() {
const currentImageData = await this.getCurrentImageData();
this.currentModal = await ImageModal.create();
this.root = this.currentModal.getRoot()[0];
if (currentImageData && currentImageData.src) {
this.loadPreviewImage(currentImageData.src);
} else {
this.loadInsertImage();
}
}
/**
* Displays an insert image view asynchronously.
*
* @returns {Promise<void>}
*/
loadInsertImage = async function() {
const templateContext = {
elementid: this.editor.id,
showfilepicker: this.canShowFilePicker,
showdropzone: this.canShowDropZone,
};
Promise.all([bodyImageInsert(templateContext, this.root), footerImageInsert(templateContext, this.root)])
.then(() => {
const imageinsert = new ImageInsert(
this.root,
this.editor,
this.currentModal,
this.canShowFilePicker,
this.canShowDropZone,
);
imageinsert.init();
return;
})
.catch(error => {
window.console.log(error);
});
};
async getTemplateContext(data) {
return {
elementid: this.editor.id,
showfilepicker: this.canShowFilePicker,
...data,
};
}
async getCurrentImageData() {
const selectedImageProperties = this.getSelectedImageProperties();
if (!selectedImageProperties) {
return {};
}
const properties = {...selectedImageProperties};
if (properties.src) {
properties.haspreview = true;
}
if (!properties.alt) {
properties.presentation = true;
}
return properties;
}
/**
* Asynchronously loads and previews an image from the provided URL.
*
* @param {string} url - The URL of the image to load and preview.
* @returns {Promise<void>}
*/
loadPreviewImage = async function(url) {
this.startImageLoading();
const image = new Image();
image.src = url;
image.addEventListener('error', async() => {
const urlWarningLabelEle = this.root.querySelector(Selectors.IMAGE.elements.urlWarning);
urlWarningLabelEle.innerHTML = await getString('imageurlrequired', 'tiny_mediacms');
showElements(Selectors.IMAGE.elements.urlWarning, this.root);
this.stopImageLoading();
});
image.addEventListener('load', async() => {
const currentImageData = await this.getCurrentImageData();
let templateContext = await this.getTemplateContext(currentImageData);
templateContext.sizecustomhelpicon = {text: await getString('sizecustom_help', 'tiny_mediacms')};
Promise.all([bodyImageDetails(templateContext, this.root), footerImageDetails(templateContext, this.root)])
.then(() => {
this.stopImageLoading();
return;
})
.then(() => {
const imagedetails = new ImageDetails(
this.root,
this.editor,
this.currentModal,
this.canShowFilePicker,
this.canShowDropZone,
url,
image,
);
imagedetails.init();
return;
})
.catch(error => {
window.console.log(error);
});
});
};
getSelectedImageProperties() {
const image = this.getSelectedImage();
if (!image) {
this.selectedImage = null;
return null;
}
const properties = {
src: null,
alt: null,
width: null,
height: null,
presentation: false,
customStyle: '', // Custom CSS styles applied to the image.
};
const getImageHeight = (image) => {
if (!isPercentageValue(String(image.height))) {
return parseInt(image.height, 10);
}
return image.height;
};
const getImageWidth = (image) => {
if (!isPercentageValue(String(image.width))) {
return parseInt(image.width, 10);
}
return image.width;
};
// Get the current selection.
this.selectedImage = image;
properties.customStyle = image.style.cssText;
const width = getImageWidth(image);
if (width !== 0) {
properties.width = width;
}
const height = getImageHeight(image);
if (height !== 0) {
properties.height = height;
}
properties.src = image.getAttribute('src');
properties.alt = image.getAttribute('alt') || '';
properties.presentation = (image.getAttribute('role') === 'presentation');
return properties;
}
getSelectedImage() {
const imgElm = this.editor.selection.getNode();
const figureElm = this.editor.dom.getParent(imgElm, 'figure.image');
if (figureElm) {
return this.editor.dom.select('img', figureElm)[0];
}
if (imgElm && (imgElm.nodeName.toUpperCase() !== 'IMG' || this.isPlaceholderImage(imgElm))) {
return null;
}
return imgElm;
}
isPlaceholderImage(imgElm) {
if (imgElm.nodeName.toUpperCase() !== 'IMG') {
return false;
}
return (imgElm.hasAttribute('data-mce-object') || imgElm.hasAttribute('data-mce-placeholder'));
}
/**
* Displays the upload loader and disables UI elements while loading a file.
*/
startImageLoading() {
showElements(Selectors.IMAGE.elements.loaderIcon, this.root);
hideElements(Selectors.IMAGE.elements.insertImage, this.root);
}
/**
* Displays the upload loader and disables UI elements while loading a file.
*/
stopImageLoading() {
hideElements(Selectors.IMAGE.elements.loaderIcon, this.root);
showElements(Selectors.IMAGE.elements.insertImage, this.root);
}
}

View File

@@ -0,0 +1,614 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Tiny media plugin image details class for Moodle.
*
* @module tiny_mediacms/imagedetails
* @copyright 2024 Meirza <meirza.arson@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Config from 'core/config';
import ModalEvents from 'core/modal_events';
import Notification from 'core/notification';
import Pending from 'core/pending';
import Selectors from './selectors';
import Templates from 'core/templates';
import {getString} from 'core/str';
import {ImageInsert} from 'tiny_mediacms/imageinsert';
import {
bodyImageInsert,
footerImageInsert,
showElements,
hideElements,
isPercentageValue,
} from 'tiny_mediacms/imagehelpers';
export class ImageDetails {
DEFAULTS = {
WIDTH: 160,
HEIGHT: 160,
};
rawImageDimensions = null;
constructor(
root,
editor,
currentModal,
canShowFilePicker,
canShowDropZone,
currentUrl,
image,
) {
this.root = root;
this.editor = editor;
this.currentModal = currentModal;
this.canShowFilePicker = canShowFilePicker;
this.canShowDropZone = canShowDropZone;
this.currentUrl = currentUrl;
this.image = image;
}
init = function() {
this.currentModal.setTitle(getString('imagedetails', 'tiny_mediacms'));
this.imageTypeChecked();
this.presentationChanged();
this.storeImageDimensions(this.image);
this.setImageDimensions();
this.registerEventListeners();
};
/**
* Loads and displays a preview image based on the provided URL, and handles image loading events.
*/
loadInsertImage = async function() {
const templateContext = {
elementid: this.editor.id,
showfilepicker: this.canShowFilePicker,
showdropzone: this.canShowDropZone,
};
Promise.all([bodyImageInsert(templateContext, this.root), footerImageInsert(templateContext, this.root)])
.then(() => {
const imageinsert = new ImageInsert(
this.root,
this.editor,
this.currentModal,
this.canShowFilePicker,
this.canShowDropZone,
);
imageinsert.init();
return;
})
.catch(error => {
window.console.log(error);
});
};
storeImageDimensions(image) {
// Store dimensions of the raw image, falling back to defaults for images without dimensions (e.g. SVG).
this.rawImageDimensions = {
width: image.width || this.DEFAULTS.WIDTH,
height: image.height || this.DEFAULTS.HEIGHT,
};
const getCurrentWidth = (element) => {
if (element.value === '') {
element.value = this.rawImageDimensions.width;
}
return element.value;
};
const getCurrentHeight = (element) => {
if (element.value === '') {
element.value = this.rawImageDimensions.height;
}
return element.value;
};
const widthInput = this.root.querySelector(Selectors.IMAGE.elements.width);
const currentWidth = getCurrentWidth(widthInput);
const heightInput = this.root.querySelector(Selectors.IMAGE.elements.height);
const currentHeight = getCurrentHeight(heightInput);
const preview = this.root.querySelector(Selectors.IMAGE.elements.preview);
preview.setAttribute('src', image.src);
preview.style.display = '';
// Ensure the checkbox always in unchecked status when an image loads at first.
const constrain = this.root.querySelector(Selectors.IMAGE.elements.constrain);
if (isPercentageValue(currentWidth) && isPercentageValue(currentHeight)) {
constrain.checked = currentWidth === currentHeight;
} else if (image.width === 0 || image.height === 0) {
// If we don't have both dimensions of the image, we can't auto-size it, so disable control.
constrain.disabled = 'disabled';
} else {
// This is the same as comparing to 3 decimal places.
const widthRatio = Math.round(100 * parseInt(currentWidth, 10) / image.width);
const heightRatio = Math.round(100 * parseInt(currentHeight, 10) / image.height);
constrain.checked = widthRatio === heightRatio;
}
/**
* Sets the selected size option based on current width and height values.
*
* @param {number} currentWidth - The current width value.
* @param {number} currentHeight - The current height value.
*/
const setSelectedSize = (currentWidth, currentHeight) => {
if (this.rawImageDimensions.width === currentWidth &&
this.rawImageDimensions.height === currentHeight
) {
this.currentWidth = this.rawImageDimensions.width;
this.currentHeight = this.rawImageDimensions.height;
this.sizeChecked('original');
} else {
this.currentWidth = currentWidth;
this.currentHeight = currentHeight;
this.sizeChecked('custom');
}
};
setSelectedSize(Number(currentWidth), Number(currentHeight));
}
/**
* Handles the selection of image size options and updates the form inputs accordingly.
*
* @param {string} option - The selected image size option ("original" or "custom").
*/
sizeChecked(option) {
const widthInput = this.root.querySelector(Selectors.IMAGE.elements.width);
const heightInput = this.root.querySelector(Selectors.IMAGE.elements.height);
if (option === "original") {
this.sizeOriginalChecked();
widthInput.value = this.rawImageDimensions.width;
heightInput.value = this.rawImageDimensions.height;
} else if (option === "custom") {
this.sizeCustomChecked();
widthInput.value = this.currentWidth;
heightInput.value = this.currentHeight;
// If the current size is equal to the original size, then check the Keep proportion checkbox.
if (this.currentWidth === this.rawImageDimensions.width && this.currentHeight === this.rawImageDimensions.height) {
const constrainField = this.root.querySelector(Selectors.IMAGE.elements.constrain);
constrainField.checked = true;
}
}
this.autoAdjustSize();
}
autoAdjustSize(forceHeight = false) {
// If we do not know the image size, do not do anything.
if (!this.rawImageDimensions) {
return;
}
const widthField = this.root.querySelector(Selectors.IMAGE.elements.width);
const heightField = this.root.querySelector(Selectors.IMAGE.elements.height);
const normalizeFieldData = (fieldData) => {
fieldData.isPercentageValue = !!isPercentageValue(fieldData.field.value);
if (fieldData.isPercentageValue) {
fieldData.percentValue = parseInt(fieldData.field.value, 10);
fieldData.pixelSize = this.rawImageDimensions[fieldData.type] / 100 * fieldData.percentValue;
} else {
fieldData.pixelSize = parseInt(fieldData.field.value, 10);
fieldData.percentValue = fieldData.pixelSize / this.rawImageDimensions[fieldData.type] * 100;
}
return fieldData;
};
const getKeyField = () => {
const getValue = () => {
if (forceHeight) {
return {
field: heightField,
type: 'height',
};
} else {
return {
field: widthField,
type: 'width',
};
}
};
const currentValue = getValue();
if (currentValue.field.value === '') {
currentValue.field.value = this.rawImageDimensions[currentValue.type];
}
return normalizeFieldData(currentValue);
};
const getRelativeField = () => {
if (forceHeight) {
return normalizeFieldData({
field: widthField,
type: 'width',
});
} else {
return normalizeFieldData({
field: heightField,
type: 'height',
});
}
};
// Now update with the new values.
const constrainField = this.root.querySelector(Selectors.IMAGE.elements.constrain);
if (constrainField.checked) {
const keyField = getKeyField();
const relativeField = getRelativeField();
// We are keeping the image in proportion.
// Calculate the size for the relative field.
if (keyField.isPercentageValue) {
// In proportion, so the percentages are the same.
relativeField.field.value = keyField.field.value;
relativeField.percentValue = keyField.percentValue;
} else {
relativeField.pixelSize = Math.round(
keyField.pixelSize / this.rawImageDimensions[keyField.type] * this.rawImageDimensions[relativeField.type]
);
relativeField.field.value = relativeField.pixelSize;
}
}
// Store the custom width and height to reuse.
this.currentWidth = Number(widthField.value) !== this.rawImageDimensions.width ? widthField.value : this.currentWidth;
this.currentHeight = Number(heightField.value) !== this.rawImageDimensions.height ? heightField.value : this.currentHeight;
}
/**
* Sets the dimensions of the image preview element based on user input and constraints.
*/
setImageDimensions = () => {
const imagePreviewBox = this.root.querySelector(Selectors.IMAGE.elements.previewBox);
const image = this.root.querySelector(Selectors.IMAGE.elements.preview);
const widthField = this.root.querySelector(Selectors.IMAGE.elements.width);
const heightField = this.root.querySelector(Selectors.IMAGE.elements.height);
const updateImageDimensions = () => {
// Get the latest dimensions of the preview box for responsiveness.
const boxWidth = imagePreviewBox.clientWidth;
const boxHeight = imagePreviewBox.clientHeight;
// Get the new width and height for the image.
const dimensions = this.fitSquareIntoBox(widthField.value, heightField.value, boxWidth, boxHeight);
image.style.width = `${dimensions.width}px`;
image.style.height = `${dimensions.height}px`;
};
// If the client size is zero, then get the new dimensions once the modal is shown.
if (imagePreviewBox.clientWidth === 0) {
// Call the shown event.
this.currentModal.getRoot().on(ModalEvents.shown, () => {
updateImageDimensions();
});
} else {
updateImageDimensions();
}
};
/**
* Handles the selection of the "Original Size" option and updates the form elements accordingly.
*/
sizeOriginalChecked() {
this.root.querySelector(Selectors.IMAGE.elements.sizeOriginal).checked = true;
this.root.querySelector(Selectors.IMAGE.elements.sizeCustom).checked = false;
hideElements(Selectors.IMAGE.elements.properties, this.root);
}
/**
* Handles the selection of the "Custom Size" option and updates the form elements accordingly.
*/
sizeCustomChecked() {
this.root.querySelector(Selectors.IMAGE.elements.sizeOriginal).checked = false;
this.root.querySelector(Selectors.IMAGE.elements.sizeCustom).checked = true;
showElements(Selectors.IMAGE.elements.properties, this.root);
}
/**
* Handles changes in the image presentation checkbox and enables/disables the image alt text input accordingly.
*/
presentationChanged() {
const presentation = this.root.querySelector(Selectors.IMAGE.elements.presentation);
const alt = this.root.querySelector(Selectors.IMAGE.elements.alt);
alt.disabled = presentation.checked;
// Counting the image description characters.
this.handleKeyupCharacterCount();
}
/**
* This function checks whether an image URL is local (within the same website's domain) or external (from an external source).
* Depending on the result, it dynamically updates the visibility and content of HTML elements in a user interface.
* If the image is local then we only show it's filename.
* If the image is external then it will show full URL and it can be updated.
*/
imageTypeChecked() {
const regex = new RegExp(`${Config.wwwroot}`);
// True if the URL is from external, otherwise false.
const isExternalUrl = regex.test(this.currentUrl) === false;
// Hide the URL input.
hideElements(Selectors.IMAGE.elements.url, this.root);
if (!isExternalUrl) {
// Split the URL by '/' to get an array of segments.
const segments = this.currentUrl.split('/');
// Get the last segment, which should be the filename.
const filename = segments.pop().split('?')[0];
// Show the file name.
this.setFilenameLabel(decodeURI(filename));
} else {
this.setFilenameLabel(decodeURI(this.currentUrl));
}
}
/**
* Set the string for the URL label element.
*
* @param {string} label - The label text to set.
*/
setFilenameLabel(label) {
const urlLabelEle = this.root.querySelector(Selectors.IMAGE.elements.fileNameLabel);
if (urlLabelEle) {
urlLabelEle.innerHTML = label;
urlLabelEle.setAttribute("title", label);
}
}
toggleAriaInvalid(selectors, predicate) {
selectors.forEach((selector) => {
const elements = this.root.querySelectorAll(selector);
elements.forEach((element) => element.setAttribute('aria-invalid', predicate));
});
}
hasErrorUrlField() {
const urlError = this.currentUrl === '';
if (urlError) {
showElements(Selectors.IMAGE.elements.urlWarning, this.root);
} else {
hideElements(Selectors.IMAGE.elements.urlWarning, this.root);
}
this.toggleAriaInvalid([Selectors.IMAGE.elements.url], urlError);
return urlError;
}
hasErrorAltField() {
const alt = this.root.querySelector(Selectors.IMAGE.elements.alt).value;
const presentation = this.root.querySelector(Selectors.IMAGE.elements.presentation).checked;
const imageAltError = alt === '' && !presentation;
if (imageAltError) {
showElements(Selectors.IMAGE.elements.altWarning, this.root);
} else {
hideElements(Selectors.IMAGE.elements.urlWaaltWarningrning, this.root);
}
this.toggleAriaInvalid([Selectors.IMAGE.elements.alt, Selectors.IMAGE.elements.presentation], imageAltError);
return imageAltError;
}
updateWarning() {
const urlError = this.hasErrorUrlField();
const imageAltError = this.hasErrorAltField();
return urlError || imageAltError;
}
getImageContext() {
// Check if there are any accessibility issues.
if (this.updateWarning()) {
return null;
}
const classList = [];
const constrain = this.root.querySelector(Selectors.IMAGE.elements.constrain).checked;
const sizeOriginal = this.root.querySelector(Selectors.IMAGE.elements.sizeOriginal).checked;
if (constrain || sizeOriginal) {
// If the Auto size checkbox is checked or the Original size is checked, then apply the responsive class.
classList.push(Selectors.IMAGE.styles.responsive);
} else {
// Otherwise, remove it.
classList.pop(Selectors.IMAGE.styles.responsive);
}
return {
url: this.currentUrl,
alt: this.root.querySelector(Selectors.IMAGE.elements.alt).value,
width: this.root.querySelector(Selectors.IMAGE.elements.width).value,
height: this.root.querySelector(Selectors.IMAGE.elements.height).value,
presentation: this.root.querySelector(Selectors.IMAGE.elements.presentation).checked,
customStyle: this.root.querySelector(Selectors.IMAGE.elements.customStyle).value,
classlist: classList.join(' '),
};
}
setImage() {
const pendingPromise = new Pending('tiny_mediacms:setImage');
const url = this.currentUrl;
if (url === '') {
return;
}
// Check if there are any accessibility issues.
if (this.updateWarning()) {
pendingPromise.resolve();
return;
}
// Check for invalid width or height.
const width = this.root.querySelector(Selectors.IMAGE.elements.width).value;
if (!isPercentageValue(width) && isNaN(parseInt(width, 10))) {
this.root.querySelector(Selectors.IMAGE.elements.width).focus();
pendingPromise.resolve();
return;
}
const height = this.root.querySelector(Selectors.IMAGE.elements.height).value;
if (!isPercentageValue(height) && isNaN(parseInt(height, 10))) {
this.root.querySelector(Selectors.IMAGE.elements.height).focus();
pendingPromise.resolve();
return;
}
Templates.render('tiny_mediacms/image', this.getImageContext())
.then((html) => {
this.editor.insertContent(html);
this.currentModal.destroy();
pendingPromise.resolve();
return html;
})
.catch(error => {
window.console.log(error);
});
}
/**
* Deletes the image after confirming with the user and loads the insert image page.
*/
deleteImage() {
Notification.deleteCancelPromise(
getString('deleteimage', 'tiny_mediacms'),
getString('deleteimagewarning', 'tiny_mediacms'),
).then(() => {
hideElements(Selectors.IMAGE.elements.altWarning, this.root);
// Removing the image in the preview will bring the user to the insert page.
this.loadInsertImage();
return;
}).catch(error => {
window.console.log(error);
});
}
registerEventListeners() {
const submitAction = this.root.querySelector(Selectors.IMAGE.actions.submit);
submitAction.addEventListener('click', (e) => {
e.preventDefault();
this.setImage();
});
const deleteImageEle = this.root.querySelector(Selectors.IMAGE.actions.deleteImage);
deleteImageEle.addEventListener('click', () => {
this.deleteImage();
});
deleteImageEle.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
this.deleteImage();
}
});
this.root.addEventListener('change', (e) => {
const presentationEle = e.target.closest(Selectors.IMAGE.elements.presentation);
if (presentationEle) {
this.presentationChanged();
}
const constrainEle = e.target.closest(Selectors.IMAGE.elements.constrain);
if (constrainEle) {
this.autoAdjustSize();
}
const sizeOriginalEle = e.target.closest(Selectors.IMAGE.elements.sizeOriginal);
if (sizeOriginalEle) {
this.sizeChecked('original');
}
const sizeCustomEle = e.target.closest(Selectors.IMAGE.elements.sizeCustom);
if (sizeCustomEle) {
this.sizeChecked('custom');
}
});
this.root.addEventListener('blur', (e) => {
if (e.target.nodeType === Node.ELEMENT_NODE) {
const presentationEle = e.target.closest(Selectors.IMAGE.elements.presentation);
if (presentationEle) {
this.presentationChanged();
}
}
}, true);
// Character count.
this.root.addEventListener('keyup', (e) => {
const altEle = e.target.closest(Selectors.IMAGE.elements.alt);
if (altEle) {
this.handleKeyupCharacterCount();
}
});
this.root.addEventListener('input', (e) => {
const widthEle = e.target.closest(Selectors.IMAGE.elements.width);
if (widthEle) {
// Avoid empty value.
widthEle.value = widthEle.value === "" ? 0 : Number(widthEle.value);
this.autoAdjustSize();
}
const heightEle = e.target.closest(Selectors.IMAGE.elements.height);
if (heightEle) {
// Avoid empty value.
heightEle.value = heightEle.value === "" ? 0 : Number(heightEle.value);
this.autoAdjustSize(true);
}
});
}
handleKeyupCharacterCount() {
const alt = this.root.querySelector(Selectors.IMAGE.elements.alt).value;
const current = this.root.querySelector('#currentcount');
current.innerHTML = alt.length;
}
/**
* Calculates the dimensions to fit a square into a specified box while maintaining aspect ratio.
*
* @param {number} squareWidth - The width of the square.
* @param {number} squareHeight - The height of the square.
* @param {number} boxWidth - The width of the box.
* @param {number} boxHeight - The height of the box.
* @returns {Object} An object with the new width and height of the square to fit in the box.
*/
fitSquareIntoBox = (squareWidth, squareHeight, boxWidth, boxHeight) => {
if (squareWidth < boxWidth && squareHeight < boxHeight) {
// If the square is smaller than the box, keep its dimensions.
return {
width: squareWidth,
height: squareHeight,
};
}
// Calculate the scaling factor based on the minimum scaling required to fit in the box.
const widthScaleFactor = boxWidth / squareWidth;
const heightScaleFactor = boxHeight / squareHeight;
const minScaleFactor = Math.min(widthScaleFactor, heightScaleFactor);
// Scale the square's dimensions based on the aspect ratio and the minimum scaling factor.
const newWidth = squareWidth * minScaleFactor;
const newHeight = squareHeight * minScaleFactor;
return {
width: newWidth,
height: newHeight,
};
};
}

View File

@@ -0,0 +1,149 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Tiny media plugin image helpers.
*
* @module tiny_mediacms/imagehelpers
* @copyright 2024 Meirza <meirza.arson@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Templates from 'core/templates';
/**
* Renders and inserts the body template for inserting an image into the modal.
*
* @param {object} templateContext - The context for rendering the template.
* @param {HTMLElement} root - The root element where the template will be inserted.
* @returns {Promise<void>}
*/
export const bodyImageInsert = async(templateContext, root) => {
return Templates.renderForPromise('tiny_mediacms/insert_image_modal_insert', {...templateContext})
.then(({html, js}) => {
Templates.replaceNodeContents(root.querySelector('.tiny_imagecms_body_template'), html, js);
return;
})
.catch(error => {
window.console.log(error);
});
};
/**
* Renders and inserts the footer template for inserting an image into the modal.
*
* @param {object} templateContext - The context for rendering the template.
* @param {HTMLElement} root - The root element where the template will be inserted.
* @returns {Promise<void>}
*/
export const footerImageInsert = async(templateContext, root) => {
return Templates.renderForPromise('tiny_mediacms/insert_image_modal_insert_footer', {...templateContext})
.then(({html, js}) => {
Templates.replaceNodeContents(root.querySelector('.tiny_imagecms_footer_template'), html, js);
return;
})
.catch(error => {
window.console.log(error);
});
};
/**
* Renders and inserts the body template for displaying image details in the modal.
*
* @param {object} templateContext - The context for rendering the template.
* @param {HTMLElement} root - The root element where the template will be inserted.
* @returns {Promise<void>}
*/
export const bodyImageDetails = async(templateContext, root) => {
return Templates.renderForPromise('tiny_mediacms/insert_image_modal_details', {...templateContext})
.then(({html, js}) => {
Templates.replaceNodeContents(root.querySelector('.tiny_imagecms_body_template'), html, js);
return;
})
.catch(error => {
window.console.log(error);
});
};
/**
* Renders and inserts the footer template for displaying image details in the modal.
* @param {object} templateContext - The context for rendering the template.
* @param {HTMLElement} root - The root element where the template will be inserted.
* @returns {Promise<void>}
*/
export const footerImageDetails = async(templateContext, root) => {
return Templates.renderForPromise('tiny_mediacms/insert_image_modal_details_footer', {...templateContext})
.then(({html, js}) => {
Templates.replaceNodeContents(root.querySelector('.tiny_imagecms_footer_template'), html, js);
return;
})
.catch(error => {
window.console.log(error);
});
};
/**
* Show the element(s).
*
* @param {string|string[]} elements - The CSS selector for the elements to toggle.
* @param {object} root - The CSS selector for the elements to toggle.
*/
export const showElements = (elements, root) => {
if (elements instanceof Array) {
elements.forEach((elementSelector) => {
const element = root.querySelector(elementSelector);
if (element) {
element.classList.remove('d-none');
}
});
} else {
const element = root.querySelector(elements);
if (element) {
element.classList.remove('d-none');
}
}
};
/**
* Hide the element(s).
*
* @param {string|string[]} elements - The CSS selector for the elements to toggle.
* @param {object} root - The CSS selector for the elements to toggle.
*/
export const hideElements = (elements, root) => {
if (elements instanceof Array) {
elements.forEach((elementSelector) => {
const element = root.querySelector(elementSelector);
if (element) {
element.classList.add('d-none');
}
});
} else {
const element = root.querySelector(elements);
if (element) {
element.classList.add('d-none');
}
}
};
/**
* Checks if the given value is a percentage value.
*
* @param {string} value - The value to check.
* @returns {boolean} True if the value is a percentage value, false otherwise.
*/
export const isPercentageValue = (value) => {
return value.match(/\d+%/);
};

View File

@@ -0,0 +1,282 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Tiny media plugin image insertion class for Moodle.
*
* @module tiny_mediacms/imageinsert
* @copyright 2024 Meirza <meirza.arson@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Selectors from './selectors';
import Dropzone from 'core/dropzone';
import uploadFile from 'editor_tiny/uploader';
import {prefetchStrings} from 'core/prefetch';
import {getStrings} from 'core/str';
import {component} from "./common";
import {getFilePicker} from 'editor_tiny/options';
import {displayFilepicker} from 'editor_tiny/utils';
import {ImageDetails} from 'tiny_mediacms/imagedetails';
import {
showElements,
hideElements,
bodyImageDetails,
footerImageDetails,
} from 'tiny_mediacms/imagehelpers';
prefetchStrings('tiny_mediacms', [
'insertimage',
'enterurl',
'enterurlor',
'imageurlrequired',
'uploading',
'loading',
'addfilesdrop',
'sizecustom_help',
]);
export class ImageInsert {
constructor(
root,
editor,
currentModal,
canShowFilePicker,
canShowDropZone,
) {
this.root = root;
this.editor = editor;
this.currentModal = currentModal;
this.canShowFilePicker = canShowFilePicker;
this.canShowDropZone = canShowDropZone;
}
init = async function() {
// Get the localization lang strings and turn them into object.
const langStringKeys = [
'insertimage',
'enterurl',
'enterurlor',
'imageurlrequired',
'uploading',
'loading',
'addfilesdrop',
'sizecustom_help',
];
const langStringvalues = await getStrings([...langStringKeys].map((key) => ({key, component})));
// Convert array to object.
this.langStrings = Object.fromEntries(langStringKeys.map((key, index) => [key, langStringvalues[index]]));
this.currentModal.setTitle(this.langStrings.insertimage);
if (this.canShowDropZone) {
const dropZoneEle = document.querySelector(Selectors.IMAGE.elements.dropzoneContainer);
// Accepted types can be either a string or an array.
let acceptedTypes = getFilePicker(this.editor, 'image').accepted_types;
if (Array.isArray(acceptedTypes)) {
acceptedTypes = acceptedTypes.join(',');
}
const dropZone = new Dropzone(
dropZoneEle,
acceptedTypes,
files => {
this.handleUploadedFile(files);
}
);
dropZone.setLabel(this.langStrings.addfilesdrop);
dropZone.init();
}
await this.registerEventListeners();
};
/**
* Enables or disables the URL-related buttons in the footer based on the current URL and input value.
*/
toggleUrlButton() {
const urlInput = this.root.querySelector(Selectors.IMAGE.elements.url);
const url = urlInput.value;
const addUrl = this.root.querySelector(Selectors.IMAGE.actions.addUrl);
addUrl.disabled = !(url !== "" && this.isValidUrl(url));
}
/**
* Check if given string is a valid URL.
*
* @param {String} urlString URL the link will point to.
* @returns {boolean} True is valid, otherwise false.
*/
isValidUrl = urlString => {
const urlPattern = new RegExp('^(https?:\\/\\/)?' + // Protocol.
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // Domain name.
'((\\d{1,3}\\.){3}\\d{1,3})|localhost)' + // OR ip (v4) address, localhost.
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'); // Port and path.
return !!urlPattern.test(urlString);
};
/**
* Handles changes in the image URL input field and loads a preview of the image if the URL has changed.
*/
urlChanged() {
hideElements(Selectors.IMAGE.elements.urlWarning, this.root);
const input = this.root.querySelector(Selectors.IMAGE.elements.url);
if (input.value && input.value !== this.currentUrl) {
this.loadPreviewImage(input.value);
}
}
/**
* Loads and displays a preview image based on the provided URL, and handles image loading events.
*
* @param {string} url - The URL of the image to load and display.
*/
loadPreviewImage = function(url) {
this.startImageLoading();
this.currentUrl = url;
const image = new Image();
image.src = url;
image.addEventListener('error', () => {
const urlWarningLabelEle = this.root.querySelector(Selectors.IMAGE.elements.urlWarning);
urlWarningLabelEle.innerHTML = this.langStrings.imageurlrequired;
showElements(Selectors.IMAGE.elements.urlWarning, this.root);
this.currentUrl = "";
this.stopImageLoading();
});
image.addEventListener('load', () => {
let templateContext = {};
templateContext.sizecustomhelpicon = {text: this.langStrings.sizecustom_help};
Promise.all([bodyImageDetails(templateContext, this.root), footerImageDetails(templateContext, this.root)])
.then(() => {
const imagedetails = new ImageDetails(
this.root,
this.editor,
this.currentModal,
this.canShowFilePicker,
this.canShowDropZone,
this.currentUrl,
image,
);
imagedetails.init();
return;
}).then(() => {
this.stopImageLoading();
return;
})
.catch(error => {
window.console.log(error);
});
});
};
/**
* Displays the upload loader and disables UI elements while loading a file.
*/
startImageLoading() {
showElements(Selectors.IMAGE.elements.loaderIcon, this.root);
const elementsToHide = [
Selectors.IMAGE.elements.insertImage,
Selectors.IMAGE.elements.urlWarning,
Selectors.IMAGE.elements.modalFooter,
];
hideElements(elementsToHide, this.root);
}
/**
* Displays the upload loader and disables UI elements while loading a file.
*/
stopImageLoading() {
hideElements(Selectors.IMAGE.elements.loaderIcon, this.root);
const elementsToShow = [
Selectors.IMAGE.elements.insertImage,
Selectors.IMAGE.elements.modalFooter,
];
showElements(elementsToShow, this.root);
}
filePickerCallback(params) {
if (params.url) {
this.loadPreviewImage(params.url);
}
}
/**
* Updates the content of the loader icon.
*
* @param {HTMLElement} root - The root element containing the loader icon.
* @param {object} langStrings - An object containing language strings.
* @param {number|null} progress - The progress percentage (optional).
* @returns {void}
*/
updateLoaderIcon = (root, langStrings, progress = null) => {
const loaderIcon = root.querySelector(Selectors.IMAGE.elements.loaderIconContainer + ' div');
loaderIcon.innerHTML = progress !== null ? `${langStrings.uploading} ${Math.round(progress)}%` : langStrings.loading;
};
/**
* Handles the uploaded file, initiates the upload process, and updates the UI during the upload.
*
* @param {FileList} files - The list of files to upload (usually from a file input field).
* @returns {Promise<void>} A promise that resolves when the file is uploaded and processed.
*/
handleUploadedFile = async(files) => {
try {
this.startImageLoading();
const fileURL = await uploadFile(this.editor, 'image', files[0], files[0].name, (progress) => {
this.updateLoaderIcon(this.root, this.langStrings, progress);
});
// Set the loader icon content to "loading" after the file upload completes.
this.updateLoaderIcon(this.root, this.langStrings);
this.filePickerCallback({url: fileURL});
} catch (error) {
// Handle the error.
const urlWarningLabelEle = this.root.querySelector(Selectors.IMAGE.elements.urlWarning);
urlWarningLabelEle.innerHTML = error.error !== undefined ? error.error : error;
showElements(Selectors.IMAGE.elements.urlWarning, this.root);
this.stopImageLoading();
}
};
registerEventListeners() {
this.root.addEventListener('click', async(e) => {
const addUrlEle = e.target.closest(Selectors.IMAGE.actions.addUrl);
if (addUrlEle) {
this.urlChanged();
}
const imageBrowserAction = e.target.closest(Selectors.IMAGE.actions.imageBrowser);
if (imageBrowserAction && this.canShowFilePicker) {
e.preventDefault();
const params = await displayFilepicker(this.editor, 'image');
this.filePickerCallback(params);
}
});
this.root.addEventListener('input', (e) => {
const urlEle = e.target.closest(Selectors.IMAGE.elements.url);
if (urlEle) {
this.toggleUrlButton();
}
});
const fileInput = this.root.querySelector(Selectors.IMAGE.elements.fileInput);
if (fileInput) {
fileInput.addEventListener('change', () => {
this.handleUploadedFile(fileInput.files);
});
}
}
}

View File

@@ -0,0 +1,49 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Image Modal for Tiny.
*
* @module tiny_mediacms/imagemodal
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Modal from 'core/modal';
import {component} from './common';
export default class ImageModal extends Modal {
static TYPE = `${component}/imagemodal`;
static TEMPLATE = `${component}/insert_image_modal`;
registerEventListeners() {
// Call the parent registration.
super.registerEventListeners();
// Register to close on save/cancel.
this.registerCloseOnSave();
this.registerCloseOnCancel();
}
configure(modalConfig) {
modalConfig.large = true;
modalConfig.removeOnClose = true;
modalConfig.show = true;
super.configure(modalConfig);
}
}
ImageModal.registerModalType();

View File

@@ -0,0 +1,86 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Tiny Media Manager plugin class for Moodle.
*
* @module tiny_mediacms/manager
* @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Templates from 'core/templates';
import {getString} from 'core/str';
import Modal from 'core/modal';
import * as ModalEvents from 'core/modal_events';
import {getData} from './options';
import Config from 'core/config';
export default class MediaManager {
editor = null;
area = null;
constructor(editor) {
this.editor = editor;
const data = getData(editor);
this.area = data.params.area;
this.area.itemid = data.fpoptions.image.itemid;
}
async displayDialogue() {
const modal = await Modal.create({
large: true,
title: getString('mediamanagerproperties', 'tiny_mediacms'),
body: Templates.render('tiny_mediacms/mm2_iframe', {
src: this.getIframeURL()
}),
removeOnClose: true,
show: true,
});
modal.getRoot().on(ModalEvents.bodyRendered, () => {
this.selectFirstElement();
});
document.querySelector('.modal-lg').style.cssText = `max-width: 850px`;
return modal;
}
// It will select the first element in the file manager.
selectFirstElement() {
const iframe = document.getElementById('mm2-iframe');
iframe.addEventListener('load', function() {
let intervalId = setInterval(function() {
const iDocument = iframe.contentWindow.document;
if (iDocument.querySelector('.filemanager')) {
const firstFocusableElement = iDocument.querySelector('.fp-navbar a:not([disabled])');
if (firstFocusableElement) {
firstFocusableElement.focus();
}
clearInterval(intervalId);
}
}, 200);
});
}
getIframeURL() {
const url = new URL(`${Config.wwwroot}/lib/editor/tiny/plugins/mediacms/manage.php`);
url.searchParams.append('elementid', this.editor.getElement().id);
for (const key in this.area) {
url.searchParams.append(key, this.area[key]);
}
return url.toString();
}
}

View File

@@ -0,0 +1,117 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Options helper for Tiny Media plugin.
*
* @module tiny_mediacms/options
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {getPluginOptionName} from 'editor_tiny/options';
import {pluginName} from './common';
const dataName = getPluginOptionName(pluginName, 'data');
const permissionsName = getPluginOptionName(pluginName, 'permissions');
const ltiName = getPluginOptionName(pluginName, 'lti');
/**
* Register the options for the Tiny Media plugin.
*
* @param {TinyMCE} editor
*/
export const register = (editor) => {
const registerOption = editor.options.register;
registerOption(permissionsName, {
processor: 'object',
"default": {
image: {
filepicker: false,
}
},
});
registerOption(dataName, {
processor: 'object',
"default": {
// MediaCMS video library configuration
mediacmsApiUrl: '', // e.g., 'https://deic.mediacms.io/api/v1/media'
mediacmsBaseUrl: '', // e.g., 'https://deic.mediacms.io'
mediacmsPageSize: 12,
// Auto-conversion settings
autoConvertEnabled: true, // Enable/disable auto-conversion of pasted MediaCMS URLs
autoConvertBaseUrl: '', // Base URL to restrict auto-conversion (empty = allow all MediaCMS domains)
autoConvertOptions: {
// Default embed options for auto-converted videos
showTitle: true,
linkTitle: true,
showRelated: true,
showUserAvatar: true,
},
},
});
registerOption(ltiName, {
processor: 'object',
"default": {
// LTI configuration for MediaCMS iframe library
toolId: 0, // LTI external tool ID
courseId: 0, // Current course ID
contentItemUrl: '', // URL to /mod/lti/contentitem.php for Deep Linking
},
});
};
/**
* Get the permissions configuration for the Tiny Media plugin.
*
* @param {TinyMCE} editor
* @returns {object}
*/
export const getPermissions = (editor) => editor.options.get(permissionsName);
/**
* Get the permissions configuration for the Tiny Media plugin.
*
* @param {TinyMCE} editor
* @returns {object}
*/
export const getImagePermissions = (editor) => getPermissions(editor).image;
/**
* Get the permissions configuration for the Tiny Media plugin.
*
* @param {TinyMCE} editor
* @returns {object}
*/
export const getEmbedPermissions = (editor) => getPermissions(editor).embed;
/**
* Get the data configuration for the Media Manager.
*
* @param {TinyMCE} editor
* @returns {object}
*/
export const getData = (editor) => editor.options.get(dataName);
/**
* Get the LTI configuration for the MediaCMS iframe library.
*
* @param {TinyMCE} editor
* @returns {object}
*/
export const getLti = (editor) => editor.options.get(ltiName);

View File

@@ -0,0 +1,184 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Tiny Media plugin for Moodle.
*
* @module tiny_mediacms/plugin
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {getTinyMCE} from 'editor_tiny/loader';
import {getPluginMetadata} from 'editor_tiny/utils';
import {component, pluginName} from './common';
import * as Commands from './commands';
import * as Configuration from './configuration';
import * as Options from './options';
import {setupAutoConvert} from './autoconvert';
/**
* Check if a URL is a MediaCMS URL (embed or view).
*
* @param {string} url - The URL to check
* @returns {boolean} True if it's a MediaCMS URL
*/
const isMediaCMSUrl = (url) => {
if (!url) {
return false;
}
try {
const urlObj = new URL(url);
// Match both /embed and /view paths with ?m= parameter
return (urlObj.pathname === '/embed' || urlObj.pathname === '/view') && urlObj.searchParams.has('m');
} catch (e) {
return false;
}
};
/**
* Convert a MediaCMS URL (embed or view) to an iframe HTML string.
* If it's a view URL, it will be converted to embed URL.
*
* @param {string} url - The MediaCMS URL
* @returns {string} The iframe HTML
*/
const mediaCMSUrlToIframe = (url) => {
// Convert view URL to embed URL if needed
let embedUrl = url;
try {
const urlObj = new URL(url);
if (urlObj.pathname === '/view') {
urlObj.pathname = '/embed';
embedUrl = urlObj.toString();
}
} catch (e) {
// Keep original URL if parsing fails
}
return `<iframe src="${embedUrl}" ` +
`style="width: 100%; aspect-ratio: 16 / 9; display: block; border: 0;" ` +
`allowfullscreen="allowfullscreen"></iframe>`;
};
/**
* Regular expression to match standalone MediaCMS URLs in content.
* Matches URLs that are on their own line or surrounded by whitespace/tags.
* The URL must contain /embed?m= or /view?m= pattern.
*/
const MEDIACMS_URL_PATTERN = /(^|>|\s)(https?:\/\/[^\s<>"]+\/(?:embed|view)\?m=[^\s<>"]+)(<|\s|$)/g;
// eslint-disable-next-line no-async-promise-executor
export default new Promise(async(resolve) => {
const [
tinyMCE,
setupCommands,
pluginMetadata,
] = await Promise.all([
getTinyMCE(),
Commands.getSetup(),
getPluginMetadata(component, pluginName),
]);
tinyMCE.PluginManager.add(`${component}/plugin`, (editor) => {
// Register options.
Options.register(editor);
// Setup the Commands (buttons, menu items, and so on).
setupCommands(editor);
// Setup auto-conversion of pasted MediaCMS URLs.
setupAutoConvert(editor);
// Convert MediaCMS URLs to iframes when content is loaded into the editor.
// This handles content from the database that was saved as just URLs.
editor.on('BeforeSetContent', (e) => {
if (e.content && typeof e.content === 'string') {
// Replace standalone MediaCMS URLs with iframes
e.content = e.content.replace(MEDIACMS_URL_PATTERN, (match, before, url, after) => {
// Verify it's a valid MediaCMS URL
if (isMediaCMSUrl(url)) {
return before + mediaCMSUrlToIframe(url) + after;
}
return match;
});
}
});
// Convert MediaCMS iframes back to just embed URLs when saving.
// This stores only the URL in the database, not the full iframe HTML.
editor.on('GetContent', (e) => {
if (e.format === 'html') {
// Create a temporary container to manipulate the HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = e.content;
// Remove edit buttons
tempDiv.querySelectorAll('.tiny-mediacms-edit-btn').forEach(btn => btn.remove());
// Process all iframes - convert MediaCMS iframes to just URLs
tempDiv.querySelectorAll('iframe').forEach(iframe => {
const src = iframe.getAttribute('src');
if (isMediaCMSUrl(src)) {
// Check if iframe is inside a wrapper
const wrapper = iframe.closest('.tiny-mediacms-iframe-wrapper') ||
iframe.closest('.tiny-iframe-responsive');
// Create a text node with just the URL
const urlText = document.createTextNode(src);
// Wrap in a paragraph for proper formatting
const p = document.createElement('p');
p.appendChild(urlText);
if (wrapper) {
// Replace the entire wrapper with the URL
wrapper.parentNode.insertBefore(p, wrapper);
wrapper.remove();
} else {
// Replace just the iframe with the URL
iframe.parentNode.insertBefore(p, iframe);
iframe.remove();
}
}
});
// Clean up any remaining wrappers that might not have had MediaCMS iframes
tempDiv.querySelectorAll('.tiny-mediacms-iframe-wrapper').forEach(wrapper => {
const iframe = wrapper.querySelector('iframe');
if (iframe) {
wrapper.parentNode.insertBefore(iframe, wrapper);
}
wrapper.remove();
});
tempDiv.querySelectorAll('.tiny-iframe-responsive').forEach(wrapper => {
const iframe = wrapper.querySelector('iframe');
if (iframe) {
wrapper.parentNode.insertBefore(iframe, wrapper);
}
wrapper.remove();
});
e.content = tempDiv.innerHTML;
}
});
return pluginMetadata;
});
// Resolve the Media Plugin and include configuration.
resolve([`${component}/plugin`, Configuration]);
});

View File

@@ -0,0 +1,162 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Tiny Media plugin helper function to build queryable data selectors.
*
* @module tiny_mediacms/selectors
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export default {
IMAGE: {
actions: {
submit: '.tiny_imagecms_urlentrysubmit',
imageBrowser: '.openimagecmsbrowser',
addUrl: '.tiny_imagecms_addurl',
deleteImage: '.tiny_imagecms_deleteicon',
},
elements: {
form: 'form.tiny_imagecms_form',
alignSettings: '.tiny_imagecms_button',
alt: '.tiny_imagecms_altentry',
altWarning: '.tiny_imagecms_altwarning',
height: '.tiny_imagecms_heightentry',
width: '.tiny_imagecms_widthentry',
url: '.tiny_imagecms_urlentry',
urlWarning: '.tiny_imagecms_urlwarning',
size: '.tiny_imagecms_size',
presentation: '.tiny_imagecms_presentation',
constrain: '.tiny_imagecms_constrain',
customStyle: '.tiny_imagecms_customstyle',
preview: '.tiny_imagecms_preview',
previewBox: '.tiny_imagecms_preview_box',
loaderIcon: '.tiny_imagecms_loader',
loaderIconContainer: '.tiny_imagecms_loader_container',
insertImage: '.tiny_imagecms_insert_image',
modalFooter: '.modal-footer',
dropzoneContainer: '.tiny_imagecms_dropzone_container',
fileInput: '#tiny_imagecms_fileinput',
fileNameLabel: '.tiny_imagecms_filename',
sizeOriginal: '.tiny_imagecms_sizeoriginal',
sizeCustom: '.tiny_imagecms_sizecustom',
properties: '.tiny_imagecms_properties',
},
styles: {
responsive: 'img-fluid',
},
},
EMBED: {
actions: {
submit: '.tiny_mediacms_submit',
mediaBrowser: '.openmediacmsbrowser',
},
elements: {
form: 'form.tiny_mediacms_form',
source: '.tiny_mediacms_source',
track: '.tiny_mediacms_track',
mediaSource: '.tiny_mediacms_media_source',
linkSource: '.tiny_mediacms_link_source',
linkSize: '.tiny_mediacms_link_size',
posterSource: '.tiny_mediacms_poster_source',
posterSize: '.tiny_mediacms_poster_size',
displayOptions: '.tiny_mediacms_display_options',
name: '.tiny_mediacms_name_entry',
title: '.tiny_mediacms_title_entry',
url: '.tiny_mediacms_url_entry',
width: '.tiny_mediacms_width_entry',
height: '.tiny_mediacms_height_entry',
trackSource: '.tiny_mediacms_track_source',
trackKind: '.tiny_mediacms_track_kind_entry',
trackLabel: '.tiny_mediacms_track_label_entry',
trackLang: '.tiny_mediacms_track_lang_entry',
trackDefault: '.tiny_mediacms_track_default',
mediaControl: '.tiny_mediacms_controls',
mediaAutoplay: '.tiny_mediacms_autoplay',
mediaMute: '.tiny_mediacms_mute',
mediaLoop: '.tiny_mediacms_loop',
advancedSettings: '.tiny_mediacms_advancedsettings',
linkTab: 'li[data-medium-type="link"]',
videoTab: 'li[data-medium-type="video"]',
audioTab: 'li[data-medium-type="audio"]',
linkPane: '.tab-pane[data-medium-type="link"]',
videoPane: '.tab-pane[data-medium-type="video"]',
audioPane: '.tab-pane[data-medium-type="audio"]',
trackSubtitlesTab: 'li[data-track-kind="subtitles"]',
trackCaptionsTab: 'li[data-track-kind="captions"]',
trackDescriptionsTab: 'li[data-track-kind="descriptions"]',
trackChaptersTab: 'li[data-track-kind="chapters"]',
trackMetadataTab: 'li[data-track-kind="metadata"]',
trackSubtitlesPane: '.tab-pane[data-track-kind="subtitles"]',
trackCaptionsPane: '.tab-pane[data-track-kind="captions"]',
trackDescriptionsPane: '.tab-pane[data-track-kind="descriptions"]',
trackChaptersPane: '.tab-pane[data-track-kind="chapters"]',
trackMetadataPane: '.tab-pane[data-track-kind="metadata"]',
},
mediaTypes: {
link: 'LINK',
video: 'VIDEO',
audio: 'AUDIO',
},
trackKinds: {
subtitles: 'SUBTITLES',
captions: 'CAPTIONS',
descriptions: 'DESCRIPTIONS',
chapters: 'CHAPTERS',
metadata: 'METADATA',
},
},
IFRAME: {
actions: {
remove: '[data-action="remove"]',
},
elements: {
form: 'form.tiny_iframecms_form',
url: '.tiny_iframecms_url',
urlWarning: '.tiny_iframecms_url_warning',
showTitle: '.tiny_iframecms_showtitle',
linkTitle: '.tiny_iframecms_linktitle',
showRelated: '.tiny_iframecms_showrelated',
showUserAvatar: '.tiny_iframecms_showuseravatar',
responsive: '.tiny_iframecms_responsive',
startAt: '.tiny_iframecms_startat',
startAtEnabled: '.tiny_iframecms_startat_enabled',
aspectRatio: '.tiny_iframecms_aspectratio',
width: '.tiny_iframecms_width',
height: '.tiny_iframecms_height',
preview: '.tiny_iframecms_preview',
previewContainer: '.tiny_iframecms_preview_container',
// Tab elements
tabs: '.tiny_iframecms_tabs',
tabUrlBtn: '.tiny_iframecms_tab_url_btn',
tabIframeLibraryBtn: '.tiny_iframecms_tab_iframe_library_btn',
paneUrl: '.tiny_iframecms_pane_url',
paneIframeLibrary: '.tiny_iframecms_pane_iframe_library',
// Iframe library elements
iframeLibraryContainer: '.tiny_iframecms_iframe_library_container',
iframeLibraryPlaceholder:
'.tiny_iframecms_iframe_library_placeholder',
iframeLibraryLoading: '.tiny_iframecms_iframe_library_loading',
iframeLibraryFrame: '.tiny_iframecms_iframe_library_frame',
},
aspectRatios: {
'16:9': { width: 560, height: 315 },
'4:3': { width: 560, height: 420 },
'1:1': { width: 400, height: 400 },
custom: null,
},
},
};

View File

@@ -0,0 +1,95 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Tiny Media Manager usedfiles.
*
* @module tiny_mediacms/usedfiles
* @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import * as Templates from 'core/templates';
import Config from 'core/config';
class UsedFileManager {
constructor(files, userContext, itemId, elementId) {
this.files = files;
this.userContext = userContext;
this.itemId = itemId;
this.elementId = elementId;
}
getElementId() {
return this.elementId;
}
getUsedFiles() {
const editor = window.parent.tinymce.EditorManager.get(this.getElementId());
if (!editor) {
window.console.error(`Editor not found for ${this.getElementId()}`);
return [];
}
const content = editor.getContent();
const baseUrl = `${Config.wwwroot}/draftfile.php/${this.userContext}/user/draft/${this.itemId}/`;
const pattern = new RegExp("[\"']" + baseUrl.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + "(?<filename>.+?)[\\?\"']", 'gm');
const usedFiles = [...content.matchAll(pattern)].map((match) => decodeURIComponent(match.groups.filename));
return usedFiles;
}
// Return an array of unused files.
findUnusedFiles(usedFiles) {
return Object.entries(this.files)
.filter(([filename]) => !usedFiles.includes(filename))
.map(([filename]) => filename);
}
// Return an array of missing files.
findMissingFiles(usedFiles) {
return usedFiles.filter((filename) => !this.files.hasOwnProperty(filename));
}
updateFiles() {
const form = document.querySelector('form');
const usedFiles = this.getUsedFiles();
const unusedFiles = this.findUnusedFiles(usedFiles);
const missingFiles = this.findMissingFiles(usedFiles);
form.querySelectorAll('input[type=checkbox][name^="deletefile"]').forEach((checkbox) => {
if (!unusedFiles.includes(checkbox.dataset.filename)) {
checkbox.closest('.fitem').remove();
}
});
form.classList.toggle('has-missing-files', !!missingFiles.length);
form.classList.toggle('has-unused-files', !!unusedFiles.length);
return Templates.renderForPromise('tiny_mediacms/missingfiles', {
missingFiles,
}).then(({html, js}) => {
Templates.replaceNodeContents(form.querySelector('.missing-files'), html, js);
return;
});
}
}
export const init = (files, usercontext, itemid, elementid) => {
const manager = new UsedFileManager(files, usercontext, itemid, elementid);
manager.updateFiles();
return manager;
};

View File

@@ -0,0 +1,118 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Atto text editor manage files plugin form.
*
* @package tiny_mediacms
* @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tiny_mediacms\form;
use html_writer;
defined('MOODLE_INTERNAL') || die();
require_once("{$CFG->libdir}/formslib.php");
/**
* Form allowing to edit files in one draft area.
*
* @package tiny_mediacms
* @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class manage_files_form extends \moodleform {
public function definition() {
global $PAGE, $USER;
$mform = $this->_form;
$mform->setDisableShortforms(true);
$itemid = $this->_customdata['draftitemid'];
$elementid = $this->_customdata['elementid'];
$options = $this->_customdata['options'];
$files = $this->_customdata['files'];
$usercontext = $this->_customdata['context'];
$removeorphaneddrafts = $this->_customdata['removeorphaneddrafts'] ?? false;
$mform->addElement('header', 'filemanagerhdr', get_string('filemanager', 'tiny_mediacms'));
$mform->addElement('hidden', 'itemid');
$mform->setType('itemid', PARAM_INT);
$mform->addElement('hidden', 'maxbytes');
$mform->setType('maxbytes', PARAM_INT);
$mform->addElement('hidden', 'subdirs');
$mform->setType('subdirs', PARAM_INT);
$mform->addElement('hidden', 'accepted_types');
$mform->setType('accepted_types', PARAM_RAW);
$mform->addElement('hidden', 'return_types');
$mform->setType('return_types', PARAM_INT);
$mform->addElement('hidden', 'context');
$mform->setType('context', PARAM_INT);
$mform->addElement('hidden', 'areamaxbytes');
$mform->setType('areamaxbytes', PARAM_INT);
$mform->addElement('hidden', 'elementid');
$mform->setType('elementid', PARAM_TEXT);
$mform->addElement('filemanager', 'files_filemanager', '', null, $options);
// Let the user know that any drafts not referenced in the text will be removed automatically.
if ($removeorphaneddrafts) {
$mform->addElement('static', '', '', html_writer::tag(
'div',
get_string('unusedfilesremovalnotice', 'tiny_mediacms')
));
}
$mform->addElement('header', 'missingfileshdr', get_string('missingfiles', 'tiny_mediacms'));
$mform->addElement('static', '', '',
html_writer::tag(
'div',
html_writer::tag('div', get_string('hasmissingfiles', 'tiny_mediacms')) .
html_writer::tag('div', '', ['class' => 'missing-files']
),
['class' => 'file-status'])
);
$mform->addElement('header', 'deletefileshdr', get_string('unusedfilesheader', 'tiny_mediacms'));
$mform->addElement('static', '', '', html_writer::tag('div', get_string('unusedfilesdesc', 'tiny_mediacms')));
foreach ($files as $hash => $file) {
$mform->addElement('checkbox', "deletefile[{$hash}]", '', $file, ['data-filename' => $file]);
$mform->setType("deletefile[{$hash}]", PARAM_INT);
}
$mform->addElement('submit', 'delete', get_string('deleteselected', 'tiny_mediacms'));
$PAGE->requires->js_call_amd('tiny_mediacms/usedfiles', 'init', [
'files' => array_flip($files),
'usercontext' => $usercontext->id,
'itemid' => $itemid,
'elementid' => $elementid,
]);
}
}

View File

@@ -0,0 +1,226 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace tiny_mediacms;
use context;
use context_course;
use context_module;
use editor_tiny\editor;
use editor_tiny\plugin;
use editor_tiny\plugin_with_buttons;
use editor_tiny\plugin_with_configuration;
use editor_tiny\plugin_with_menuitems;
use moodle_url;
/**
* Tiny media plugin.
*
* @package tiny_media
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class plugininfo extends plugin implements plugin_with_buttons, plugin_with_menuitems, plugin_with_configuration {
/**
* Whether the plugin is enabled
*
* @param context $context The context that the editor is used within
* @param array $options The options passed in when requesting the editor
* @param array $fpoptions The filepicker options passed in when requesting the editor
* @param editor $editor The editor instance in which the plugin is initialised
* @return boolean
*/
public static function is_enabled(
context $context,
array $options,
array $fpoptions,
?editor $editor = null
): bool {
// Disabled if:
// - Not logged in or guest.
// - Files are not allowed.
// - Only URL are supported.
$canhavefiles = !empty($options['maxfiles']);
$canhaveexternalfiles = !empty($options['return_types']) && ($options['return_types'] & FILE_EXTERNAL);
return isloggedin() && !isguestuser() && ($canhavefiles || $canhaveexternalfiles);
}
public static function get_available_buttons(): array {
return [
'tiny_mediacms/tiny_mediacms_image',
'tiny_mediacms/tiny_mediacms_video',
'tiny_mediacms/tiny_mediacms_iframe',
];
}
public static function get_available_menuitems(): array {
return [
'tiny_mediacms/tiny_mediacms_image',
'tiny_mediacms/tiny_mediacms_video',
'tiny_mediacms/tiny_mediacms_iframe',
];
}
public static function get_plugin_configuration_for_context(
context $context,
array $options,
array $fpoptions,
?editor $editor = null
): array {
// TODO Fetch the actual permissions.
$permissions = [
'image' => [
'filepicker' => true,
],
'embed' => [
'filepicker' => true,
]
];
// Get LTI configuration for MediaCMS iframe library.
$lticonfig = self::get_lti_configuration($context);
// Get auto-convert configuration.
$autoconvertconfig = self::get_autoconvert_configuration();
return array_merge([
'permissions' => $permissions,
], self::get_file_manager_configuration($context, $options, $fpoptions), $lticonfig, $autoconvertconfig);
}
/**
* Get the auto-convert configuration for pasted MediaCMS URLs.
*
* @return array Auto-convert configuration data
*/
protected static function get_autoconvert_configuration(): array {
$baseurl = get_config('tiny_mediacms', 'autoconvert_baseurl');
// Helper function to get config with default value of true.
$getboolconfig = function($name) {
$value = get_config('tiny_mediacms', $name);
// If the setting hasn't been saved yet (false/empty), default to true.
// Only return false if explicitly set to '0'.
return $value !== '0' && $value !== 0;
};
return [
'data' => [
'autoConvertEnabled' => true, // Always enabled
'autoConvertBaseUrl' => !empty($baseurl) ? $baseurl : '',
'autoConvertOptions' => [
'showTitle' => $getboolconfig('autoconvert_showtitle'),
'linkTitle' => $getboolconfig('autoconvert_linktitle'),
'showRelated' => $getboolconfig('autoconvert_showrelated'),
'showUserAvatar' => $getboolconfig('autoconvert_showuseravatar'),
],
],
];
}
/**
* Get the LTI configuration for the MediaCMS iframe library.
*
* @param context $context The context that the editor is used within
* @return array LTI configuration data
*/
protected static function get_lti_configuration(context $context): array {
global $COURSE;
// Get the configured LTI tool ID from plugin settings.
$ltitoolid = get_config('tiny_mediacms', 'ltitoolid');
// Determine the course ID from context.
$courseid = 0;
if ($context instanceof context_course) {
$courseid = $context->instanceid;
} else if ($context instanceof context_module) {
// Get the course from the module context.
$coursecontext = $context->get_course_context(false);
if ($coursecontext) {
$courseid = $coursecontext->instanceid;
}
} else if (!empty($COURSE->id) && $COURSE->id != SITEID) {
// Fall back to the global $COURSE if available and not the site.
$courseid = $COURSE->id;
}
// Build the content item URL for LTI Deep Linking.
// This URL initiates the LTI Deep Linking flow which allows users
// to select content (like videos) from the tool provider.
$contentitemurl = '';
if (!empty($ltitoolid) && $courseid > 0) {
$contentitemurl = (new moodle_url('/mod/lti/contentitem.php', [
'id' => $ltitoolid,
'course' => $courseid,
'title' => 'MediaCMS Library',
'return_types' => 1 // LTI_DEEPLINKING_RETURN_TYPE_LTI_LINK
]))->out(false);
}
return [
'lti' => [
'toolId' => !empty($ltitoolid) ? (int) $ltitoolid : 0,
'courseId' => $courseid,
'contentItemUrl' => $contentitemurl,
],
];
}
protected static function get_file_manager_configuration(
context $context,
array $options,
array $fpoptions
): array {
global $USER;
$params = [
'area' => [],
'usercontext' => \context_user::instance($USER->id)->id,
];
$keys = [
'itemid',
'areamaxbytes',
'maxbytes',
'subdirs',
'return_types',
'removeorphaneddrafts',
];
if (isset($options['context'])) {
if (is_object($options['context'])) {
$params['area']['context'] = $options['context']->id;
} else {
$params['area']['context'] = $options['context'];
}
}
foreach ($keys as $key) {
if (isset($options[$key])) {
$params['area'][$key] = $options[$key];
}
}
return [
'storeinrepo' => true,
'data' => [
'params' => $params,
'fpoptions' => $fpoptions,
],
];
}
}

View File

@@ -0,0 +1,30 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace tiny_mediacms\privacy;
/**
* Privacy Subsystem implementation for the media plugin for TinyMCE.
*
* @package tiny_media
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
public static function get_reason(): string {
return 'privacy:metadata';
}
}

View File

@@ -0,0 +1,7 @@
alignment,tiny_media
alignment_bottom,tiny_media
alignment_left,tiny_media
alignment_middle,tiny_media
alignment_right,tiny_media
alignment_top,tiny_media
helplinktext,tiny_media

View File

@@ -0,0 +1,199 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for component 'tiny_mediacms', language 'en'.
*
* @package tiny_mediacms
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['addcaptionstrack'] = 'Add caption track';
$string['addchapterstrack'] = 'Add chapter track';
$string['adddescriptionstrack'] = 'Add description track';
$string['addfilesdrop'] = 'Drag and drop an image to upload, or click to select';
$string['addmetadatatrack'] = 'Add metadata track';
$string['addsource_help'] = 'You are recommended to provide an alternative media source, as desktop and mobile browsers support different file formats.';
$string['addsource'] = 'Add alternative source';
$string['addsubtitlestrack'] = 'Add subtitle track';
$string['addurl'] = 'Add';
$string['advancedsettings'] = 'Advanced settings';
$string['audio'] = 'Audio';
$string['audiosourcelabel'] = 'Audio source URL';
$string['autoplay'] = 'Play automatically';
$string['browserepositories'] = 'Browse repositories...';
$string['browserepositoriesimage'] = 'Browse repositories';
$string['captions_help'] = 'Captions may be used to describe everything happening in the track, including non-verbal sounds such as a phone ringing.';
$string['captions'] = 'Captions';
$string['captionssourcelabel'] = 'Caption track URL';
$string['chapters_help'] = 'Chapter titles may be provided for use in navigating the media resource.';
$string['chapters'] = 'Chapters';
$string['chapterssourcelabel'] = 'Chapter track URL';
$string['constrain'] = 'Keep proportion';
$string['controls'] = 'Show controls';
$string['createmedia'] = 'Insert favorite media';
$string['default'] = 'Default';
$string['deleteimage'] = 'Delete image';
$string['deleteimagewarning'] = 'Are you sure you want to remove the image?';
$string['deleteselected'] = 'Delete selected files';
$string['descriptions_help'] = 'Audio descriptions may be used to provide a narration which explains visual details not apparent from the audio alone.';
$string['descriptions'] = 'Descriptions';
$string['descriptionssourcelabel'] = 'Description track URL';
$string['displayoptions'] = 'Display options';
$string['enteralt'] = 'How would you describe this image to someone who can\'t see it?';
$string['entername'] = 'Name';
$string['entersource'] = 'Source URL';
$string['entertitle'] = 'Title';
$string['enterurl'] = 'Add via URL';
$string['enterurlor'] = 'Or add via URL';
$string['filemanager'] = 'Favorite file manager';
$string['hasmissingfiles'] = 'Warning! The following files that are referenced in the text area appear to be missing:';
$string['height'] = 'Height';
$string['imagebuttontitle'] = 'Favorite Image';
$string['imagedetails'] = 'Image details';
$string['imageproperties'] = 'Image properties';
$string['imageurlrequired'] = 'An image must have a valid URL.';
$string['insertimage'] = 'Insert favorite image';
$string['label'] = 'Label';
$string['languagesavailable'] = 'Languages available';
$string['languagesinstalled'] = 'Languages installed';
$string['link'] = 'Link';
$string['loading'] = 'Preparing the image';
$string['loop'] = 'Loop';
$string['managefiles'] = 'Manage favorite files';
$string['mediabuttontitle'] = 'Favorite Multimedia';
$string['mediamanagerbuttontitle'] = 'Favorite media manager';
$string['mediamanagerproperties'] = 'Favorite media manager';
$string['metadata_help'] = 'Metadata tracks, for use from a script, may be used only if the player supports metadata.';
$string['metadata'] = 'Metadata';
$string['metadatasourcelabel'] = 'Metadata track URL';
$string['missingfiles'] = 'Missing files';
$string['mute'] = 'Muted';
$string['pluginname'] = 'MediaCMS';
$string['presentation'] = 'This image is decorative only';
$string['presentationoraltrequired'] = 'An image must have a description, unless it is marked as decorative only.';
$string['privacy:metadata'] = 'The favorite media plugin for TinyMCE does not store any personal data.';
$string['remove'] = 'Remove';
$string['repositorynotpermitted'] = 'Paste an image link in the field below.';
$string['repositoryuploadnotpermitted'] = 'Paste an image link in the field below or<br>click the Browse Repositories button.';
$string['saveimage'] = 'Save';
$string['size'] = 'Width x height (in pixels)';
$string['sizecustom'] = 'Custom size';
$string['sizecustom_help'] = 'This image is just a preview. Changes to its size will be visible after you save it.';
$string['sizeoriginal'] = 'Original size';
$string['srclang'] = 'Language';
$string['subtitles_help'] = 'Subtitles may be used to provide a transcription or translation of the dialogue.';
$string['subtitles'] = 'Subtitles';
$string['subtitlessourcelabel'] = 'Subtitle track URL';
$string['tracks_help'] = 'Subtitles, captions, chapters and descriptions can be added via a WebVTT (Web Video Text Tracks) format file. Track labels will be shown in the selection drop-down menu. For each type of track, any track set as default will be pre-selected at the start of the video.';
$string['tracks'] = 'Subtitles and captions';
$string['unusedfilesdesc'] = 'The following embedded files are not used in the text area:';
$string['unusedfilesheader'] = 'Unused files';
$string['unusedfilesremovalnotice'] = 'Any unused files will be automatically deleted when saving changes.';
$string['updatemedia'] = 'Update favorite media';
$string['uploading'] = 'Uploading';
$string['video'] = 'Video';
$string['videoheight'] = 'Video height';
$string['videosourcelabel'] = 'Video source URL';
$string['videowidth'] = 'Video width';
$string['width'] = 'Width';
// Iframe embed strings.
$string['iframebuttontitle'] = 'Insert MediaCMS Media';
$string['iframemodaltitle'] = 'Insert MediaCMS Media';
$string['iframeurl'] = 'MediaCMS Video URL or embed code';
$string['iframeurlplaceholder'] = 'Paste MediaCMS Video URL or iframe embed code';
$string['iframeurlinvalid'] = 'Please enter a valid MediaCMS Video URL or embed code';
$string['embedoptions'] = 'Embed Options';
$string['showtitle'] = 'Show title';
$string['linktitle'] = 'Link title';
$string['showrelated'] = 'Show related';
$string['showuseravatar'] = 'Show user avatar';
$string['responsive'] = 'Responsive';
$string['startat'] = 'Start at';
$string['aspectratio'] = 'Aspect Ratio';
$string['aspectratio_16_9'] = '16:9';
$string['aspectratio_4_3'] = '4:3';
$string['aspectratio_1_1'] = '1:1';
$string['aspectratio_custom'] = 'Custom';
$string['dimensions'] = 'Dimensions';
$string['preview'] = 'Preview';
$string['insertiframe'] = 'Insert video';
$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['tabvideolibrary'] = 'Video Library';
$string['tabvideolibraryiframe'] = 'Media Library';
// Video library strings.
$string['librarysearchplaceholder'] = 'Search videos...';
$string['librarysortnewest'] = 'Newest first';
$string['librarysortoldest'] = 'Oldest first';
$string['librarysorttitle'] = 'Title A-Z';
$string['librarysortviews'] = 'Most views';
$string['libraryloading'] = 'Loading videos...';
$string['libraryempty'] = 'No videos found';
$string['libraryerror'] = 'Failed to load videos';
$string['libraryretry'] = 'Retry';
$string['libraryprev'] = 'Previous';
$string['librarynext'] = 'Next';
$string['libraryselect'] = 'Select';
$string['librarypage'] = 'Page {$a->current} of {$a->total}';
$string['libraryvideoselected'] = 'Video selected. Configure embed options below.';
// LTI settings strings.
$string['ltitoolid'] = 'LTI Tool';
$string['ltitoolid_desc'] = 'Select the External Tool configuration for MediaCMS. This enables the authenticated video library in the editor.';
$string['noltitoolsfound'] = 'No LTI tools found';
$string['choose'] = 'Choose...';
$string['ltitoolid_help'] = 'To find the LTI tool ID, go to Site administration > Plugins > Activity modules > External tool > Manage tools. The ID is shown in the URL when editing a tool (e.g., id=2).';
// Iframe library from LTI strings.
$string['iframelibraryloading'] = 'Loading MediaCMS video library...';
$string['iframelibraryplaceholder'] = 'Click here to load the video library';
$string['iframelibraryerror'] = 'Failed to load the video library. Please check your LTI configuration.';
$string['iframelibrarynotconfigured'] = 'The MediaCMS LTI tool has not been configured. Please contact your administrator.';
// Auto-convert settings strings.
$string['autoconvertheading'] = 'Auto-convert MediaCMS URLs';
$string['autoconvertheading_desc'] = 'Configure automatic conversion of pasted MediaCMS URLs to embedded videos.';
$string['autoconvertenabled'] = 'Enable auto-convert';
$string['autoconvertenabled_desc'] = 'When enabled, pasting a MediaCMS video URL (e.g., https://lti.mediacms.io/view?m=VIDEO_ID) into the editor will automatically convert it to an embedded video player.';
$string['autoconvert_baseurl'] = 'MediaCMS URL';
$string['autoconvert_baseurl_desc'] = 'The base URL of your MediaCMS instance (e.g., https://lti.mediacms.io). If specified, only URLs from this domain will be auto-converted. Leave empty to allow any MediaCMS URL.';
$string['autoconvert_showtitle'] = 'Show video title';
$string['autoconvert_showtitle_desc'] = 'Display the video title in the embedded player.';
$string['autoconvert_linktitle'] = 'Link video title';
$string['autoconvert_linktitle_desc'] = 'Make the video title clickable, linking to the original video page.';
$string['autoconvert_showrelated'] = 'Show related videos';
$string['autoconvert_showrelated_desc'] = 'Display related videos after the current video ends.';
$string['autoconvert_showuseravatar'] = 'Show user avatar';
$string['autoconvert_showuseravatar_desc'] = 'Display the uploader\'s avatar in the embedded player.';
// Deprecated since Moodle 4.4.
$string['alignment'] = 'Alignment';
$string['alignment_bottom'] = 'Bottom';
$string['alignment_left'] = 'Left';
$string['alignment_middle'] = 'Middle';
$string['alignment_right'] = 'Right';
$string['alignment_top'] = 'Top';
// Deprecated since Moodle 4.5.
$string['helplinktext'] = 'Favorite media helper';

View File

@@ -0,0 +1,148 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Manage files in user draft area attached to texteditor.
*
* @package tiny_mediacms
* @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require(__DIR__ . '/../../../../../config.php');
require_once($CFG->libdir . '/filestorage/file_storage.php');
require_once($CFG->dirroot . '/repository/lib.php');
$itemid = required_param('itemid', PARAM_INT) ?? 0;
$maxbytes = optional_param('maxbytes', 0, PARAM_INT);
$subdirs = optional_param('subdirs', 0, PARAM_INT);
$acceptedtypes = optional_param('accepted_types', '*', PARAM_RAW); // TODO Not yet passed to this script.
$returntypes = optional_param('return_types', null, PARAM_INT);
$areamaxbytes = optional_param('areamaxbytes', FILE_AREA_MAX_BYTES_UNLIMITED, PARAM_INT);
$contextid = optional_param('context', SYSCONTEXTID, PARAM_INT);
$elementid = optional_param('elementid', '', PARAM_TEXT);
$removeorphaneddrafts = optional_param('removeorphaneddrafts', 0, PARAM_INT);
$context = context::instance_by_id($contextid);
if ($context->contextlevel == CONTEXT_MODULE) {
// Module context.
$cm = $DB->get_record('course_modules', ['id' => $context->instanceid]);
require_login($cm->course, true, $cm);
} else if (($coursecontext = $context->get_course_context(false)) && $coursecontext->id != SITEID) {
// Course context or block inside the course.
require_login($coursecontext->instanceid);
$PAGE->set_context($context);
} else {
// Block that is not inside the course, user or system context.
require_login();
$PAGE->set_context($context);
}
// Guests can never manage files.
if (isguestuser()) {
throw new \moodle_exception('noguest');
}
$title = get_string('managefiles', 'tiny_mediacms');
$url = new moodle_url('/lib/editor/tiny/plugins/mediacms/manage.php', [
'itemid' => $itemid,
'maxbytes' => $maxbytes,
'subdirs' => $subdirs,
'accepted_types' => $acceptedtypes,
'return_types' => $returntypes,
'areamaxbytes' => $areamaxbytes,
'context' => $contextid,
'elementid' => $elementid,
'removeorphaneddrafts' => $removeorphaneddrafts,
]);
$PAGE->set_url($url);
$PAGE->set_title($title);
$PAGE->set_heading($title);
$PAGE->set_pagelayout('popup');
if ($returntypes !== null) {
// Links are allowed in textarea but never allowed in filemanager.
$returntypes = $returntypes & ~FILE_EXTERNAL;
}
// These are the options required for the filepicker.
$options = [
'subdirs' => $subdirs,
'maxbytes' => $maxbytes,
'maxfiles' => -1,
'accepted_types' => $acceptedtypes,
'areamaxbytes' => $areamaxbytes,
'return_types' => $returntypes,
'context' => $context
];
$usercontext = context_user::instance($USER->id);
$fs = get_file_storage();
$files = $fs->get_directory_files($usercontext->id, 'user', 'draft', $itemid, '/', !empty($subdirs), false);
$filenames = [];
foreach ($files as $file) {
$filenames[$file->get_pathnamehash()] = ltrim($file->get_filepath(), '/') . $file->get_filename();
}
$mform = new tiny_mediacms\form\manage_files_form(null, [
'context' => $usercontext,
'options' => $options,
'draftitemid' => $itemid,
'files' => $filenames,
'elementid' => $elementid,
'removeorphaneddrafts' => $removeorphaneddrafts,
], 'post', '', [
'id' => 'tiny_mediacms_form',
]
);
if ($data = $mform->get_data()) {
if (!empty($data->deletefile)) {
foreach (array_keys($data->deletefile) as $filehash) {
if ($file = $fs->get_file_by_hash($filehash)) {
// Make sure the user didn't modify the filehash to delete another file.
if ($file->get_component() !== 'user' || $file->get_filearea() !== 'draft') {
// The file must belong to the user/draft area.
continue;
}
if ($file->get_contextid() != $usercontext->id) {
// The user must own the file - that is it must be in their user draft file area.
continue;
}
if ($file->get_itemid() != $itemid) {
// It must be the file they requested be deleted.
continue;
}
$file->delete();
}
}
}
// Redirect to prevent re-posting the form.
redirect($url);
}
$mform->set_data(array_merge($options, [
'files_filemanager' => $itemid,
'itemid' => $itemid,
'elementid' => $elementid,
'context' => $context->id,
]));
echo $OUTPUT->header();
$mform->display();
echo $OUTPUT->footer();

View File

@@ -0,0 +1,206 @@
# MediaCMS URL Auto-Convert Feature
This feature automatically converts pasted MediaCMS video URLs into embedded video players within the TinyMCE editor.
## Overview
When a user pastes a MediaCMS video URL like:
```
https://deic.mediacms.io/view?m=JpBd1Zvdl
```
It is automatically converted to an embedded video player:
```html
<div class="tiny-iframe-responsive" contenteditable="false">
<iframe
style="width: 100%; max-width: calc(100vh * 16 / 9); aspect-ratio: 16 / 9; display: block; margin: auto; border: 0;"
src="https://deic.mediacms.io/embed?m=JpBd1Zvdl&showTitle=1&showRelated=1&showUserAvatar=1&linkTitle=1"
allowfullscreen="allowfullscreen">
</iframe>
</div>
```
## Supported URL Formats
The auto-convert feature recognizes MediaCMS view URLs in this format:
- `https://[domain]/view?m=[VIDEO_ID]`
Examples:
- `https://deic.mediacms.io/view?m=JpBd1Zvdl`
- `https://your-mediacms-instance.com/view?m=abc123`
## Configuration
### Accessing Settings
1. Log in to Moodle as an administrator
2. Navigate to: **Site administration****Plugins****Text editors****TinyMCE editor****MediaCMS**
3. Scroll to the **Auto-convert MediaCMS URLs** section
### Available Settings
| Setting | Description | Default |
|---------|-------------|---------|
| **Enable auto-convert** | Turn the auto-convert feature on or off | Enabled |
| **MediaCMS base URL** | Restrict auto-conversion to a specific MediaCMS domain | Empty (allow all) |
| **Show video title** | Display the video title in the embedded player | Enabled |
| **Link video title** | Make the video title clickable, linking to the original video page | Enabled |
| **Show related videos** | Display related videos after the current video ends | Enabled |
| **Show user avatar** | Display the uploader's avatar in the embedded player | Enabled |
### Settings Location in Moodle
The settings are stored in the Moodle database under the `tiny_mediacms` plugin configuration:
- `tiny_mediacms/autoconvertenabled` - Enable/disable auto-convert
- `tiny_mediacms/autoconvert_baseurl` - MediaCMS base URL (e.g., https://deic.mediacms.io)
- `tiny_mediacms/autoconvert_showtitle` - Show title option
- `tiny_mediacms/autoconvert_linktitle` - Link title option
- `tiny_mediacms/autoconvert_showrelated` - Show related option
- `tiny_mediacms/autoconvert_showuseravatar` - Show user avatar option
### Base URL Configuration
The **MediaCMS base URL** setting controls which MediaCMS instances are recognized for auto-conversion:
- **Empty (default)**: Any MediaCMS URL will be auto-converted (e.g., URLs from any `*/view?m=*` pattern)
- **Specific URL**: Only URLs from the specified domain will be auto-converted
Example configurations:
- `https://deic.mediacms.io` - Only convert URLs from deic.mediacms.io
- `https://media.myuniversity.edu` - Only convert URLs from your institution's MediaCMS
## Technical Details
### File Structure
```
amd/src/
├── autoconvert.js # Main auto-convert module
├── plugin.js # Plugin initialization (imports autoconvert)
└── options.js # Configuration options definition
classes/
└── plugininfo.php # Passes PHP settings to JavaScript
settings.php # Admin settings page definition
lang/en/
└── tiny_mediacms.php # Language strings for settings
```
### How It Works
1. **Paste Detection**: The `autoconvert.js` module listens for `paste` events on the TinyMCE editor
2. **URL Validation**: When text is pasted, it checks if it matches the MediaCMS URL pattern
3. **HTML Generation**: If valid, it generates the responsive iframe HTML with configured options
4. **Content Insertion**: The original URL is replaced with the embedded video
### JavaScript Configuration
The settings are passed from PHP to JavaScript via the `plugininfo.php` class:
```php
protected static function get_autoconvert_configuration(): array {
$baseurl = get_config('tiny_mediacms', 'autoconvert_baseurl');
return [
'data' => [
'autoConvertEnabled' => (bool) get_config('tiny_mediacms', 'autoconvertenabled'),
'autoConvertBaseUrl' => !empty($baseurl) ? $baseurl : '',
'autoConvertOptions' => [
'showTitle' => (bool) get_config('tiny_mediacms', 'autoconvert_showtitle'),
'linkTitle' => (bool) get_config('tiny_mediacms', 'autoconvert_linktitle'),
'showRelated' => (bool) get_config('tiny_mediacms', 'autoconvert_showrelated'),
'showUserAvatar' => (bool) get_config('tiny_mediacms', 'autoconvert_showuseravatar'),
],
],
];
}
```
### Default Values (in options.js)
If PHP settings are not configured, the JavaScript uses these defaults:
```javascript
registerOption(dataName, {
processor: 'object',
"default": {
autoConvertEnabled: true,
autoConvertBaseUrl: '', // Empty = allow all MediaCMS domains
autoConvertOptions: {
showTitle: true,
linkTitle: true,
showRelated: true,
showUserAvatar: true,
},
},
});
```
## Customization
### Disabling Auto-Convert
To disable the feature entirely:
1. Go to the plugin settings (see "Accessing Settings" above)
2. Uncheck **Enable auto-convert**
3. Save changes
### Programmatic Configuration
You can also set these values directly in the database using Moodle's `set_config()` function:
```php
// Disable auto-convert
set_config('autoconvertenabled', 0, 'tiny_mediacms');
// Set the MediaCMS base URL (restrict to specific domain)
set_config('autoconvert_baseurl', 'https://deic.mediacms.io', 'tiny_mediacms');
// Customize embed options
set_config('autoconvert_showtitle', 1, 'tiny_mediacms');
set_config('autoconvert_linktitle', 0, 'tiny_mediacms');
set_config('autoconvert_showrelated', 0, 'tiny_mediacms');
set_config('autoconvert_showuseravatar', 1, 'tiny_mediacms');
```
### CLI Configuration
Using Moodle CLI:
```bash
# Enable auto-convert
php admin/cli/cfg.php --component=tiny_mediacms --name=autoconvertenabled --set=1
# Set the MediaCMS base URL
php admin/cli/cfg.php --component=tiny_mediacms --name=autoconvert_baseurl --set=https://deic.mediacms.io
# Disable showing related videos
php admin/cli/cfg.php --component=tiny_mediacms --name=autoconvert_showrelated --set=0
```
## Troubleshooting
### Auto-convert not working
1. **Check if enabled**: Verify the setting is enabled in plugin settings
2. **Clear caches**: Purge all caches (Site administration → Development → Purge all caches)
3. **Check URL format**: Ensure the URL matches the pattern `https://[domain]/view?m=[VIDEO_ID]`
4. **Browser console**: Check for JavaScript errors in the browser developer console
### Rebuilding JavaScript
If you modify the source files, rebuild using:
```bash
cd /path/to/moodle
npx grunt amd --root=public/lib/editor/tiny/plugins/mediacms
```
Note: Requires Node.js 22.x or compatible version as specified in Moodle's requirements.
## Version History
- **1.0.0** - Initial implementation of auto-convert feature

View File

@@ -0,0 +1,15 @@
define("tiny_mediacms/autoconvert",["exports","./options"],(function(_exports,_options){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setupAutoConvert=_exports.isMediaCMSUrl=_exports.convertToEmbed=void 0;
/**
* Tiny MediaCMS Auto-convert module.
*
* This module automatically converts pasted MediaCMS URLs into embedded videos.
* When a user pastes a MediaCMS video URL (e.g., https://deic.mediacms.io/view?m=JpBd1Zvdl),
* it will be automatically converted to an iframe embed.
*
* @module tiny_mediacms/autoconvert
* @copyright 2024
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const MEDIACMS_VIEW_URL_PATTERN=/^(https?:\/\/[^\/]+)\/view\?m=([a-zA-Z0-9_-]+)$/,parseMediaCMSUrl=text=>{if(!text||"string"!=typeof text)return null;const trimmed=text.trim(),match=trimmed.match(MEDIACMS_VIEW_URL_PATTERN);return match?{baseUrl:match[1],videoId:match[2],originalUrl:trimmed}:null},isDomainAllowed=(parsed,config)=>{const configuredBaseUrl=config.autoConvertBaseUrl||config.mediacmsBaseUrl;if(!configuredBaseUrl)return!0;try{const configuredUrl=new URL(configuredBaseUrl),pastedUrl=new URL(parsed.baseUrl);return configuredUrl.host===pastedUrl.host}catch(e){return!0}},generateEmbedHtml=function(parsed){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const embedUrl=new URL("".concat(parsed.baseUrl,"/embed"));embedUrl.searchParams.set("m",parsed.videoId),embedUrl.searchParams.set("showTitle",!1!==options.showTitle?"1":"0"),embedUrl.searchParams.set("showRelated",!1!==options.showRelated?"1":"0"),embedUrl.searchParams.set("showUserAvatar",!1!==options.showUserAvatar?"1":"0"),embedUrl.searchParams.set("linkTitle",!1!==options.linkTitle?"1":"0");const html='<iframe width="400" height="300" style="display: block; border: 0;" '+'src="'.concat(embedUrl.toString(),'" ')+'allowfullscreen="allowfullscreen"></iframe>';return html};_exports.setupAutoConvert=editor=>{const config=(0,_options.getData)(editor)||{};!1!==config.autoConvertEnabled&&(editor.on("paste",(e=>{handlePasteEvent(editor,e,config)})),editor.on("input",(e=>{handleInputEvent(editor,e,config)})))};const handlePasteEvent=(editor,e,config)=>{const clipboardData=e.clipboardData||window.clipboardData;if(!clipboardData)return;const text=clipboardData.getData("text/plain")||clipboardData.getData("text");if(!text)return;const parsed=parseMediaCMSUrl(text);if(!parsed)return;if(!isDomainAllowed(parsed,config))return;e.preventDefault(),e.stopPropagation();const embedHtml=generateEmbedHtml(parsed,config.autoConvertOptions||{});setTimeout((()=>{editor.insertContent(embedHtml),editor.selection.collapse(!1)}),0)},handleInputEvent=(editor,e,config)=>{if("insertFromPaste"!==e.inputType&&"insertText"!==e.inputType)return;const node=editor.selection.getNode();if(!node||"P"!==node.nodeName)return;const text=node.textContent||"",parsed=parseMediaCMSUrl(text);if(!parsed||!isDomainAllowed(parsed,config))return;const trimmedHtml=node.innerHTML.trim();if(trimmedHtml!==text.trim()&&!trimmedHtml.startsWith(text.trim()))return;const embedHtml=generateEmbedHtml(parsed,config.autoConvertOptions||{});setTimeout((()=>{const currentText=node.textContent||"",currentParsed=parseMediaCMSUrl(currentText);currentParsed&&currentParsed.originalUrl===parsed.originalUrl&&(editor.selection.select(node),editor.insertContent(embedHtml))}),100)};_exports.isMediaCMSUrl=text=>null!==parseMediaCMSUrl(text);_exports.convertToEmbed=function(url){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const parsed=parseMediaCMSUrl(url);return parsed?generateEmbedHtml(parsed,options):null}}));
//# sourceMappingURL=autoconvert.min.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
define("tiny_mediacms/common",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={pluginName:"tiny_mediacms/plugin",component:"tiny_mediacms",iframeButtonName:"tiny_mediacms_iframe",iframeMenuItemName:"tiny_mediacms_iframe",iframeIcon:"tiny_mediacms_iframe"},_exports.default}));
//# sourceMappingURL=common.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"common.min.js","sources":["../src/common.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Media common values.\n *\n * @module tiny_mediacms/common\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n pluginName: 'tiny_mediacms/plugin',\n component: 'tiny_mediacms',\n iframeButtonName: 'tiny_mediacms_iframe',\n iframeMenuItemName: 'tiny_mediacms_iframe',\n iframeIcon: 'tiny_mediacms_iframe',\n};\n"],"names":["pluginName","component","iframeButtonName","iframeMenuItemName","iframeIcon"],"mappings":"sKAuBe,CACXA,WAAY,uBACZC,UAAW,gBACXC,iBAAkB,uBAClBC,mBAAoB,uBACpBC,WAAY"}

View File

@@ -0,0 +1,3 @@
define("tiny_mediacms/configuration",["exports","./common","editor_tiny/utils"],(function(_exports,_common,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.configure=void 0;_exports.configure=instanceConfig=>{return{contextmenu:(0,_utils.addContextmenuItem)(instanceConfig.contextmenu,_common.iframeButtonName),menu:(menu=instanceConfig.menu,menu.insert.items="".concat(_common.iframeMenuItemName," ").concat(menu.insert.items),menu),toolbar:(toolbar=instanceConfig.toolbar,toolbar.map((section=>("content"===section.name&&section.items.unshift(_common.iframeButtonName),section))))};var toolbar,menu}}));
//# sourceMappingURL=configuration.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"configuration.min.js","sources":["../src/configuration.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Media configuration.\n *\n * @module tiny_mediacms/configuration\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {\n iframeButtonName,\n iframeMenuItemName,\n} from './common';\nimport {\n addContextmenuItem,\n} from 'editor_tiny/utils';\n\nconst configureMenu = (menu) => {\n // Add the Iframe Embed to the insert menu.\n menu.insert.items = `${iframeMenuItemName} ${menu.insert.items}`;\n\n return menu;\n};\n\nconst configureToolbar = (toolbar) => {\n // The toolbar contains an array of named sections.\n // The Moodle integration ensures that there is a section called 'content'.\n\n return toolbar.map((section) => {\n if (section.name === 'content') {\n // Insert the iframe button at the start of it.\n section.items.unshift(iframeButtonName);\n }\n\n return section;\n });\n};\n\nexport const configure = (instanceConfig) => {\n // Update the instance configuration to add the Iframe Embed menu option to the menus and toolbars.\n return {\n contextmenu: addContextmenuItem(instanceConfig.contextmenu, iframeButtonName),\n menu: configureMenu(instanceConfig.menu),\n toolbar: configureToolbar(instanceConfig.toolbar),\n };\n};\n"],"names":["instanceConfig","contextmenu","iframeButtonName","menu","insert","items","iframeMenuItemName","toolbar","map","section","name","unshift"],"mappings":"wNAoD0BA,uBAEf,CACHC,aAAa,6BAAmBD,eAAeC,YAAaC,0BAC5DC,MAzBeA,KAyBKH,eAAeG,KAvBvCA,KAAKC,OAAOC,gBAAWC,uCAAsBH,KAAKC,OAAOC,OAElDF,MAsBHI,SAnBkBA,QAmBQP,eAAeO,QAftCA,QAAQC,KAAKC,UACK,YAAjBA,QAAQC,MAERD,QAAQJ,MAAMM,QAAQT,0BAGnBO,aAVWF,IAAAA,QAPHJ"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
define("tiny_mediacms/embedmodal",["exports","core/modal","./common"],(function(_exports,_modal,_common){var obj;function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=(obj=_modal)&&obj.__esModule?obj:{default:obj};class EmbedModal extends _modal.default{registerEventListeners(){super.registerEventListeners(),this.registerCloseOnSave(),this.registerCloseOnCancel()}configure(modalConfig){modalConfig.large=!0,modalConfig.removeOnClose=!0,modalConfig.show=!0,super.configure(modalConfig)}}return _exports.default=EmbedModal,_defineProperty(EmbedModal,"TYPE","".concat(_common.component,"/modal")),_defineProperty(EmbedModal,"TEMPLATE","".concat(_common.component,"/embed_media_modal")),_exports.default}));
//# sourceMappingURL=embedmodal.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"embedmodal.min.js","sources":["../src/embedmodal.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Embedded Media Management Modal for Tiny.\n *\n * @module tiny_mediacms/embedmodal\n * @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from 'core/modal';\nimport {component} from './common';\n\nexport default class EmbedModal extends Modal {\n static TYPE = `${component}/modal`;\n static TEMPLATE = `${component}/embed_media_modal`;\n\n registerEventListeners() {\n // Call the parent registration.\n super.registerEventListeners();\n\n // Register to close on save/cancel.\n this.registerCloseOnSave();\n this.registerCloseOnCancel();\n }\n\n configure(modalConfig) {\n modalConfig.large = true;\n modalConfig.removeOnClose = true;\n modalConfig.show = true;\n\n super.configure(modalConfig);\n }\n}\n"],"names":["EmbedModal","Modal","registerEventListeners","registerCloseOnSave","registerCloseOnCancel","configure","modalConfig","large","removeOnClose","show","component"],"mappings":"iaA0BqBA,mBAAmBC,eAIpCC,+BAEUA,8BAGDC,2BACAC,wBAGTC,UAAUC,aACNA,YAAYC,OAAQ,EACpBD,YAAYE,eAAgB,EAC5BF,YAAYG,MAAO,QAEbJ,UAAUC,iEAlBHN,4BACAU,6CADAV,gCAEIU"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
define("tiny_mediacms/iframemodal",["exports","core/modal","./common"],(function(_exports,_modal,_common){var obj;function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=(obj=_modal)&&obj.__esModule?obj:{default:obj};class IframeModal extends _modal.default{registerEventListeners(){super.registerEventListeners(),this.registerCloseOnSave(),this.registerCloseOnCancel()}configure(modalConfig){modalConfig.large=!0,modalConfig.removeOnClose=!0,modalConfig.show=!0,super.configure(modalConfig)}}return _exports.default=IframeModal,_defineProperty(IframeModal,"TYPE","".concat(_common.component,"/iframemodal")),_defineProperty(IframeModal,"TEMPLATE","".concat(_common.component,"/iframe_embed_modal")),_exports.default}));
//# sourceMappingURL=iframemodal.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"iframemodal.min.js","sources":["../src/iframemodal.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Iframe Embed Modal for Tiny Media2.\n *\n * @module tiny_mediacms/iframemodal\n * @copyright 2024\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from 'core/modal';\nimport {component} from './common';\n\nexport default class IframeModal extends Modal {\n static TYPE = `${component}/iframemodal`;\n static TEMPLATE = `${component}/iframe_embed_modal`;\n\n registerEventListeners() {\n // Call the parent registration.\n super.registerEventListeners();\n\n // Register to close on save/cancel.\n this.registerCloseOnSave();\n this.registerCloseOnCancel();\n }\n\n configure(modalConfig) {\n modalConfig.large = true;\n modalConfig.removeOnClose = true;\n modalConfig.show = true;\n\n super.configure(modalConfig);\n }\n}\n"],"names":["IframeModal","Modal","registerEventListeners","registerCloseOnSave","registerCloseOnCancel","configure","modalConfig","large","removeOnClose","show","component"],"mappings":"kaA0BqBA,oBAAoBC,eAIrCC,+BAEUA,8BAGDC,2BACAC,wBAGTC,UAAUC,aACNA,YAAYC,OAAQ,EACpBD,YAAYE,eAAgB,EAC5BF,YAAYG,MAAO,QAEbJ,UAAUC,kEAlBHN,6BACAU,mDADAV,iCAEIU"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,10 @@
define("tiny_mediacms/imagehelpers",["exports","core/templates"],(function(_exports,_templates){var obj;
/**
* Tiny media plugin image helpers.
*
* @module tiny_mediacms/imagehelpers
* @copyright 2024 Meirza <meirza.arson@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.showElements=_exports.isPercentageValue=_exports.hideElements=_exports.footerImageInsert=_exports.footerImageDetails=_exports.bodyImageInsert=_exports.bodyImageDetails=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};_exports.bodyImageInsert=async(templateContext,root)=>_templates.default.renderForPromise("tiny_mediacms/insert_image_modal_insert",{...templateContext}).then((_ref=>{let{html:html,js:js}=_ref;_templates.default.replaceNodeContents(root.querySelector(".tiny_imagecms_body_template"),html,js)})).catch((error=>{window.console.log(error)}));_exports.footerImageInsert=async(templateContext,root)=>_templates.default.renderForPromise("tiny_mediacms/insert_image_modal_insert_footer",{...templateContext}).then((_ref2=>{let{html:html,js:js}=_ref2;_templates.default.replaceNodeContents(root.querySelector(".tiny_imagecms_footer_template"),html,js)})).catch((error=>{window.console.log(error)}));_exports.bodyImageDetails=async(templateContext,root)=>_templates.default.renderForPromise("tiny_mediacms/insert_image_modal_details",{...templateContext}).then((_ref3=>{let{html:html,js:js}=_ref3;_templates.default.replaceNodeContents(root.querySelector(".tiny_imagecms_body_template"),html,js)})).catch((error=>{window.console.log(error)}));_exports.footerImageDetails=async(templateContext,root)=>_templates.default.renderForPromise("tiny_mediacms/insert_image_modal_details_footer",{...templateContext}).then((_ref4=>{let{html:html,js:js}=_ref4;_templates.default.replaceNodeContents(root.querySelector(".tiny_imagecms_footer_template"),html,js)})).catch((error=>{window.console.log(error)}));_exports.showElements=(elements,root)=>{if(elements instanceof Array)elements.forEach((elementSelector=>{const element=root.querySelector(elementSelector);element&&element.classList.remove("d-none")}));else{const element=root.querySelector(elements);element&&element.classList.remove("d-none")}};_exports.hideElements=(elements,root)=>{if(elements instanceof Array)elements.forEach((elementSelector=>{const element=root.querySelector(elementSelector);element&&element.classList.add("d-none")}));else{const element=root.querySelector(elements);element&&element.classList.add("d-none")}};_exports.isPercentageValue=value=>value.match(/\d+%/)}));
//# sourceMappingURL=imagehelpers.min.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
define("tiny_mediacms/imagemodal",["exports","core/modal","./common"],(function(_exports,_modal,_common){var obj;function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=(obj=_modal)&&obj.__esModule?obj:{default:obj};class ImageModal extends _modal.default{registerEventListeners(){super.registerEventListeners(),this.registerCloseOnSave(),this.registerCloseOnCancel()}configure(modalConfig){modalConfig.large=!0,modalConfig.removeOnClose=!0,modalConfig.show=!0,super.configure(modalConfig)}}return _exports.default=ImageModal,_defineProperty(ImageModal,"TYPE","".concat(_common.component,"/imagemodal")),_defineProperty(ImageModal,"TEMPLATE","".concat(_common.component,"/insert_image_modal")),ImageModal.registerModalType(),_exports.default}));
//# sourceMappingURL=imagemodal.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"imagemodal.min.js","sources":["../src/imagemodal.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Image Modal for Tiny.\n *\n * @module tiny_mediacms/imagemodal\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from 'core/modal';\nimport {component} from './common';\n\nexport default class ImageModal extends Modal {\n static TYPE = `${component}/imagemodal`;\n static TEMPLATE = `${component}/insert_image_modal`;\n\n registerEventListeners() {\n // Call the parent registration.\n super.registerEventListeners();\n\n // Register to close on save/cancel.\n this.registerCloseOnSave();\n this.registerCloseOnCancel();\n }\n\n configure(modalConfig) {\n modalConfig.large = true;\n modalConfig.removeOnClose = true;\n modalConfig.show = true;\n\n super.configure(modalConfig);\n }\n}\n\nImageModal.registerModalType();\n"],"names":["ImageModal","Modal","registerEventListeners","registerCloseOnSave","registerCloseOnCancel","configure","modalConfig","large","removeOnClose","show","component","registerModalType"],"mappings":"iaA0BqBA,mBAAmBC,eAIpCC,+BAEUA,8BAGDC,2BACAC,wBAGTC,UAAUC,aACNA,YAAYC,OAAQ,EACpBD,YAAYE,eAAgB,EAC5BF,YAAYG,MAAO,QAEbJ,UAAUC,iEAlBHN,4BACAU,kDADAV,gCAEIU,0CAoBzBV,WAAWW"}

View File

@@ -0,0 +1,3 @@
define("tiny_mediacms/manager",["exports","core/templates","core/str","core/modal","core/modal_events","./options","core/config"],(function(_exports,_templates,_str,_modal,ModalEvents,_options,_config){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=_interopRequireDefault(_templates),_modal=_interopRequireDefault(_modal),ModalEvents=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(ModalEvents),_config=_interopRequireDefault(_config);return _exports.default=class{constructor(editor){_defineProperty(this,"editor",null),_defineProperty(this,"area",null),this.editor=editor;const data=(0,_options.getData)(editor);this.area=data.params.area,this.area.itemid=data.fpoptions.image.itemid}async displayDialogue(){const modal=await _modal.default.create({large:!0,title:(0,_str.getString)("mediamanagerproperties","tiny_mediacms"),body:_templates.default.render("tiny_mediacms/mm2_iframe",{src:this.getIframeURL()}),removeOnClose:!0,show:!0});return modal.getRoot().on(ModalEvents.bodyRendered,(()=>{this.selectFirstElement()})),document.querySelector(".modal-lg").style.cssText="max-width: 850px",modal}selectFirstElement(){const iframe=document.getElementById("mm2-iframe");iframe.addEventListener("load",(function(){let intervalId=setInterval((function(){const iDocument=iframe.contentWindow.document;if(iDocument.querySelector(".filemanager")){const firstFocusableElement=iDocument.querySelector(".fp-navbar a:not([disabled])");firstFocusableElement&&firstFocusableElement.focus(),clearInterval(intervalId)}}),200)}))}getIframeURL(){const url=new URL("".concat(_config.default.wwwroot,"/lib/editor/tiny/plugins/mediacms/manage.php"));url.searchParams.append("elementid",this.editor.getElement().id);for(const key in this.area)url.searchParams.append(key,this.area[key]);return url.toString()}},_exports.default}));
//# sourceMappingURL=manager.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"manager.min.js","sources":["../src/manager.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Media Manager plugin class for Moodle.\n *\n * @module tiny_mediacms/manager\n * @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport {getString} from 'core/str';\nimport Modal from 'core/modal';\nimport * as ModalEvents from 'core/modal_events';\nimport {getData} from './options';\nimport Config from 'core/config';\n\nexport default class MediaManager {\n\n editor = null;\n area = null;\n\n constructor(editor) {\n this.editor = editor;\n const data = getData(editor);\n this.area = data.params.area;\n this.area.itemid = data.fpoptions.image.itemid;\n }\n\n async displayDialogue() {\n const modal = await Modal.create({\n large: true,\n title: getString('mediamanagerproperties', 'tiny_mediacms'),\n body: Templates.render('tiny_mediacms/mm2_iframe', {\n src: this.getIframeURL()\n }),\n removeOnClose: true,\n show: true,\n });\n modal.getRoot().on(ModalEvents.bodyRendered, () => {\n this.selectFirstElement();\n });\n\n document.querySelector('.modal-lg').style.cssText = `max-width: 850px`;\n return modal;\n }\n\n // It will select the first element in the file manager.\n selectFirstElement() {\n const iframe = document.getElementById('mm2-iframe');\n iframe.addEventListener('load', function() {\n let intervalId = setInterval(function() {\n const iDocument = iframe.contentWindow.document;\n if (iDocument.querySelector('.filemanager')) {\n const firstFocusableElement = iDocument.querySelector('.fp-navbar a:not([disabled])');\n if (firstFocusableElement) {\n firstFocusableElement.focus();\n }\n clearInterval(intervalId);\n }\n }, 200);\n });\n }\n\n getIframeURL() {\n const url = new URL(`${Config.wwwroot}/lib/editor/tiny/plugins/mediacms/manage.php`);\n url.searchParams.append('elementid', this.editor.getElement().id);\n for (const key in this.area) {\n url.searchParams.append(key, this.area[key]);\n }\n return url.toString();\n }\n}\n"],"names":["constructor","editor","data","area","params","itemid","fpoptions","image","modal","Modal","create","large","title","body","Templates","render","src","this","getIframeURL","removeOnClose","show","getRoot","on","ModalEvents","bodyRendered","selectFirstElement","document","querySelector","style","cssText","iframe","getElementById","addEventListener","intervalId","setInterval","iDocument","contentWindow","firstFocusableElement","focus","clearInterval","url","URL","Config","wwwroot","searchParams","append","getElement","id","key","toString"],"mappings":"mmDAmCIA,YAAYC,sCAHH,kCACF,WAGEA,OAASA,aACRC,MAAO,oBAAQD,aAChBE,KAAOD,KAAKE,OAAOD,UACnBA,KAAKE,OAASH,KAAKI,UAAUC,MAAMF,qCAIlCG,YAAcC,eAAMC,OAAO,CAC7BC,OAAO,EACPC,OAAO,kBAAU,yBAA0B,iBAC3CC,KAAMC,mBAAUC,OAAO,2BAA4B,CAC/CC,IAAKC,KAAKC,iBAEdC,eAAe,EACfC,MAAM,WAEVZ,MAAMa,UAAUC,GAAGC,YAAYC,cAAc,UACpCC,wBAGTC,SAASC,cAAc,aAAaC,MAAMC,2BACnCrB,MAIXiB,2BACUK,OAASJ,SAASK,eAAe,cACvCD,OAAOE,iBAAiB,QAAQ,eACxBC,WAAaC,aAAY,iBACnBC,UAAYL,OAAOM,cAAcV,YACnCS,UAAUR,cAAc,gBAAiB,OACnCU,sBAAwBF,UAAUR,cAAc,gCAClDU,uBACAA,sBAAsBC,QAE1BC,cAAcN,eAEnB,QAIXf,qBACUsB,IAAM,IAAIC,cAAOC,gBAAOC,yDAC9BH,IAAII,aAAaC,OAAO,YAAa5B,KAAKhB,OAAO6C,aAAaC,QACzD,MAAMC,OAAO/B,KAAKd,KACnBqC,IAAII,aAAaC,OAAOG,IAAK/B,KAAKd,KAAK6C,aAEpCR,IAAIS"}

View File

@@ -0,0 +1,11 @@
define("tiny_mediacms/options",["exports","editor_tiny/options","./common"],(function(_exports,_options,_common){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.register=_exports.getPermissions=_exports.getLti=_exports.getImagePermissions=_exports.getEmbedPermissions=_exports.getData=void 0;
/**
* Options helper for Tiny Media plugin.
*
* @module tiny_mediacms/options
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const dataName=(0,_options.getPluginOptionName)(_common.pluginName,"data"),permissionsName=(0,_options.getPluginOptionName)(_common.pluginName,"permissions"),ltiName=(0,_options.getPluginOptionName)(_common.pluginName,"lti");_exports.register=editor=>{const registerOption=editor.options.register;registerOption(permissionsName,{processor:"object",default:{image:{filepicker:!1}}}),registerOption(dataName,{processor:"object",default:{mediacmsApiUrl:"",mediacmsBaseUrl:"",mediacmsPageSize:12,autoConvertEnabled:!0,autoConvertBaseUrl:"",autoConvertOptions:{showTitle:!0,linkTitle:!0,showRelated:!0,showUserAvatar:!0}}}),registerOption(ltiName,{processor:"object",default:{toolId:0,courseId:0,contentItemUrl:""}})};const getPermissions=editor=>editor.options.get(permissionsName);_exports.getPermissions=getPermissions;_exports.getImagePermissions=editor=>getPermissions(editor).image;_exports.getEmbedPermissions=editor=>getPermissions(editor).embed;_exports.getData=editor=>editor.options.get(dataName);_exports.getLti=editor=>editor.options.get(ltiName)}));
//# sourceMappingURL=options.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"options.min.js","sources":["../src/options.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Options helper for Tiny Media plugin.\n *\n * @module tiny_mediacms/options\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getPluginOptionName} from 'editor_tiny/options';\nimport {pluginName} from './common';\n\nconst dataName = getPluginOptionName(pluginName, 'data');\nconst permissionsName = getPluginOptionName(pluginName, 'permissions');\nconst ltiName = getPluginOptionName(pluginName, 'lti');\n\n/**\n * Register the options for the Tiny Media plugin.\n *\n * @param {TinyMCE} editor\n */\nexport const register = (editor) => {\n const registerOption = editor.options.register;\n\n registerOption(permissionsName, {\n processor: 'object',\n \"default\": {\n image: {\n filepicker: false,\n }\n },\n });\n\n registerOption(dataName, {\n processor: 'object',\n \"default\": {\n // MediaCMS video library configuration\n mediacmsApiUrl: '', // e.g., 'https://deic.mediacms.io/api/v1/media'\n mediacmsBaseUrl: '', // e.g., 'https://deic.mediacms.io'\n mediacmsPageSize: 12,\n // Auto-conversion settings\n autoConvertEnabled: true, // Enable/disable auto-conversion of pasted MediaCMS URLs\n autoConvertBaseUrl: '', // Base URL to restrict auto-conversion (empty = allow all MediaCMS domains)\n autoConvertOptions: {\n // Default embed options for auto-converted videos\n showTitle: true,\n linkTitle: true,\n showRelated: true,\n showUserAvatar: true,\n },\n },\n });\n\n registerOption(ltiName, {\n processor: 'object',\n \"default\": {\n // LTI configuration for MediaCMS iframe library\n toolId: 0, // LTI external tool ID\n courseId: 0, // Current course ID\n contentItemUrl: '', // URL to /mod/lti/contentitem.php for Deep Linking\n },\n });\n};\n\n/**\n * Get the permissions configuration for the Tiny Media plugin.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getPermissions = (editor) => editor.options.get(permissionsName);\n\n/**\n * Get the permissions configuration for the Tiny Media plugin.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getImagePermissions = (editor) => getPermissions(editor).image;\n\n/**\n * Get the permissions configuration for the Tiny Media plugin.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getEmbedPermissions = (editor) => getPermissions(editor).embed;\n\n/**\n * Get the data configuration for the Media Manager.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getData = (editor) => editor.options.get(dataName);\n\n/**\n * Get the LTI configuration for the MediaCMS iframe library.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getLti = (editor) => editor.options.get(ltiName);\n"],"names":["dataName","pluginName","permissionsName","ltiName","editor","registerOption","options","register","processor","image","filepicker","mediacmsApiUrl","mediacmsBaseUrl","mediacmsPageSize","autoConvertEnabled","autoConvertBaseUrl","autoConvertOptions","showTitle","linkTitle","showRelated","showUserAvatar","toolId","courseId","contentItemUrl","getPermissions","get","embed"],"mappings":";;;;;;;;MA0BMA,UAAW,gCAAoBC,mBAAY,QAC3CC,iBAAkB,gCAAoBD,mBAAY,eAClDE,SAAU,gCAAoBF,mBAAY,yBAOvBG,eACfC,eAAiBD,OAAOE,QAAQC,SAEtCF,eAAeH,gBAAiB,CAC5BM,UAAW,iBACA,CACPC,MAAO,CACHC,YAAY,MAKxBL,eAAeL,SAAU,CACrBQ,UAAW,iBACA,CAEPG,eAAgB,GAChBC,gBAAiB,GACjBC,iBAAkB,GAElBC,oBAAoB,EACpBC,mBAAoB,GACpBC,mBAAoB,CAEhBC,WAAW,EACXC,WAAW,EACXC,aAAa,EACbC,gBAAgB,MAK5Bf,eAAeF,QAAS,CACpBK,UAAW,iBACA,CAEPa,OAAQ,EACRC,SAAU,EACVC,eAAgB,aAWfC,eAAkBpB,QAAWA,OAAOE,QAAQmB,IAAIvB,qFAQzBE,QAAWoB,eAAepB,QAAQK,mCAQlCL,QAAWoB,eAAepB,QAAQsB,uBAQ9CtB,QAAWA,OAAOE,QAAQmB,IAAIzB,0BAQ/BI,QAAWA,OAAOE,QAAQmB,IAAItB"}

View File

@@ -0,0 +1,10 @@
define("tiny_mediacms/plugin",["exports","editor_tiny/loader","editor_tiny/utils","./common","./commands","./configuration","./options","./autoconvert"],(function(_exports,_loader,_utils,_common,Commands,Configuration,Options,_autoconvert){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}
/**
* Tiny Media plugin for Moodle.
*
* @module tiny_mediacms/plugin
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,Commands=_interopRequireWildcard(Commands),Configuration=_interopRequireWildcard(Configuration),Options=_interopRequireWildcard(Options);var _default=new Promise((async resolve=>{const[tinyMCE,setupCommands,pluginMetadata]=await Promise.all([(0,_loader.getTinyMCE)(),Commands.getSetup(),(0,_utils.getPluginMetadata)(_common.component,_common.pluginName)]);tinyMCE.PluginManager.add("".concat(_common.component,"/plugin"),(editor=>(Options.register(editor),setupCommands(editor),(0,_autoconvert.setupAutoConvert)(editor),editor.on("GetContent",(e=>{if("html"===e.format){const tempDiv=document.createElement("div");tempDiv.innerHTML=e.content,tempDiv.querySelectorAll(".tiny-mediacms-edit-btn").forEach((btn=>btn.remove())),tempDiv.querySelectorAll(".tiny-mediacms-iframe-wrapper").forEach((wrapper=>{const iframe=wrapper.querySelector("iframe");iframe&&wrapper.parentNode.insertBefore(iframe,wrapper),wrapper.remove()})),tempDiv.querySelectorAll(".tiny-iframe-responsive").forEach((wrapper=>{const iframe=wrapper.querySelector("iframe");iframe&&wrapper.parentNode.insertBefore(iframe,wrapper),wrapper.remove()})),e.content=tempDiv.innerHTML}})),pluginMetadata))),resolve(["".concat(_common.component,"/plugin"),Configuration])}));return _exports.default=_default,_exports.default}));
//# sourceMappingURL=plugin.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"plugin.min.js","sources":["../src/plugin.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Media plugin for Moodle.\n *\n * @module tiny_mediacms/plugin\n * @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {getTinyMCE} from 'editor_tiny/loader';\nimport {getPluginMetadata} from 'editor_tiny/utils';\n\nimport {component, pluginName} from './common';\nimport * as Commands from './commands';\nimport * as Configuration from './configuration';\nimport * as Options from './options';\nimport {setupAutoConvert} from './autoconvert';\n\n// eslint-disable-next-line no-async-promise-executor\nexport default new Promise(async(resolve) => {\n const [\n tinyMCE,\n setupCommands,\n pluginMetadata,\n ] = await Promise.all([\n getTinyMCE(),\n Commands.getSetup(),\n getPluginMetadata(component, pluginName),\n ]);\n\n tinyMCE.PluginManager.add(`${component}/plugin`, (editor) => {\n // Register options.\n Options.register(editor);\n\n // Setup the Commands (buttons, menu items, and so on).\n setupCommands(editor);\n\n // Setup auto-conversion of pasted MediaCMS URLs.\n setupAutoConvert(editor);\n\n // Clean up editor-only elements before content is saved.\n // Remove wrapper divs and edit buttons that are only for the editor UI.\n editor.on('GetContent', (e) => {\n if (e.format === 'html') {\n // Create a temporary container to manipulate the HTML\n const tempDiv = document.createElement('div');\n tempDiv.innerHTML = e.content;\n\n // Remove edit buttons\n tempDiv.querySelectorAll('.tiny-mediacms-edit-btn').forEach(btn => btn.remove());\n\n // Unwrap iframes from tiny-mediacms-iframe-wrapper\n tempDiv.querySelectorAll('.tiny-mediacms-iframe-wrapper').forEach(wrapper => {\n const iframe = wrapper.querySelector('iframe');\n if (iframe) {\n wrapper.parentNode.insertBefore(iframe, wrapper);\n }\n wrapper.remove();\n });\n\n // Unwrap iframes from tiny-iframe-responsive\n tempDiv.querySelectorAll('.tiny-iframe-responsive').forEach(wrapper => {\n const iframe = wrapper.querySelector('iframe');\n if (iframe) {\n wrapper.parentNode.insertBefore(iframe, wrapper);\n }\n wrapper.remove();\n });\n\n e.content = tempDiv.innerHTML;\n }\n });\n\n return pluginMetadata;\n });\n\n // Resolve the Media Plugin and include configuration.\n resolve([`${component}/plugin`, Configuration]);\n});\n"],"names":["Promise","async","tinyMCE","setupCommands","pluginMetadata","all","Commands","getSetup","component","pluginName","PluginManager","add","editor","Options","register","on","e","format","tempDiv","document","createElement","innerHTML","content","querySelectorAll","forEach","btn","remove","wrapper","iframe","querySelector","parentNode","insertBefore","resolve","Configuration"],"mappings":";;;;;;;2OAgCe,IAAIA,SAAQC,MAAAA,gBAEnBC,QACAC,cACAC,sBACMJ,QAAQK,IAAI,EAClB,wBACAC,SAASC,YACT,4BAAkBC,kBAAWC,sBAGjCP,QAAQQ,cAAcC,cAAOH,8BAAqBI,SAE9CC,QAAQC,SAASF,QAGjBT,cAAcS,0CAGGA,QAIjBA,OAAOG,GAAG,cAAeC,OACJ,SAAbA,EAAEC,OAAmB,OAEfC,QAAUC,SAASC,cAAc,OACvCF,QAAQG,UAAYL,EAAEM,QAGtBJ,QAAQK,iBAAiB,2BAA2BC,SAAQC,KAAOA,IAAIC,WAGvER,QAAQK,iBAAiB,iCAAiCC,SAAQG,gBACxDC,OAASD,QAAQE,cAAc,UACjCD,QACAD,QAAQG,WAAWC,aAAaH,OAAQD,SAE5CA,QAAQD,YAIZR,QAAQK,iBAAiB,2BAA2BC,SAAQG,gBAClDC,OAASD,QAAQE,cAAc,UACjCD,QACAD,QAAQG,WAAWC,aAAaH,OAAQD,SAE5CA,QAAQD,YAGZV,EAAEM,QAAUJ,QAAQG,cAIrBjB,kBAIX4B,QAAQ,WAAIxB,6BAAoByB"}

View File

@@ -0,0 +1,3 @@
define("tiny_mediacms/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={IMAGE:{actions:{submit:".tiny_imagecms_urlentrysubmit",imageBrowser:".openimagecmsbrowser",addUrl:".tiny_imagecms_addurl",deleteImage:".tiny_imagecms_deleteicon"},elements:{form:"form.tiny_imagecms_form",alignSettings:".tiny_imagecms_button",alt:".tiny_imagecms_altentry",altWarning:".tiny_imagecms_altwarning",height:".tiny_imagecms_heightentry",width:".tiny_imagecms_widthentry",url:".tiny_imagecms_urlentry",urlWarning:".tiny_imagecms_urlwarning",size:".tiny_imagecms_size",presentation:".tiny_imagecms_presentation",constrain:".tiny_imagecms_constrain",customStyle:".tiny_imagecms_customstyle",preview:".tiny_imagecms_preview",previewBox:".tiny_imagecms_preview_box",loaderIcon:".tiny_imagecms_loader",loaderIconContainer:".tiny_imagecms_loader_container",insertImage:".tiny_imagecms_insert_image",modalFooter:".modal-footer",dropzoneContainer:".tiny_imagecms_dropzone_container",fileInput:"#tiny_imagecms_fileinput",fileNameLabel:".tiny_imagecms_filename",sizeOriginal:".tiny_imagecms_sizeoriginal",sizeCustom:".tiny_imagecms_sizecustom",properties:".tiny_imagecms_properties"},styles:{responsive:"img-fluid"}},EMBED:{actions:{submit:".tiny_mediacms_submit",mediaBrowser:".openmediacmsbrowser"},elements:{form:"form.tiny_mediacms_form",source:".tiny_mediacms_source",track:".tiny_mediacms_track",mediaSource:".tiny_mediacms_media_source",linkSource:".tiny_mediacms_link_source",linkSize:".tiny_mediacms_link_size",posterSource:".tiny_mediacms_poster_source",posterSize:".tiny_mediacms_poster_size",displayOptions:".tiny_mediacms_display_options",name:".tiny_mediacms_name_entry",title:".tiny_mediacms_title_entry",url:".tiny_mediacms_url_entry",width:".tiny_mediacms_width_entry",height:".tiny_mediacms_height_entry",trackSource:".tiny_mediacms_track_source",trackKind:".tiny_mediacms_track_kind_entry",trackLabel:".tiny_mediacms_track_label_entry",trackLang:".tiny_mediacms_track_lang_entry",trackDefault:".tiny_mediacms_track_default",mediaControl:".tiny_mediacms_controls",mediaAutoplay:".tiny_mediacms_autoplay",mediaMute:".tiny_mediacms_mute",mediaLoop:".tiny_mediacms_loop",advancedSettings:".tiny_mediacms_advancedsettings",linkTab:'li[data-medium-type="link"]',videoTab:'li[data-medium-type="video"]',audioTab:'li[data-medium-type="audio"]',linkPane:'.tab-pane[data-medium-type="link"]',videoPane:'.tab-pane[data-medium-type="video"]',audioPane:'.tab-pane[data-medium-type="audio"]',trackSubtitlesTab:'li[data-track-kind="subtitles"]',trackCaptionsTab:'li[data-track-kind="captions"]',trackDescriptionsTab:'li[data-track-kind="descriptions"]',trackChaptersTab:'li[data-track-kind="chapters"]',trackMetadataTab:'li[data-track-kind="metadata"]',trackSubtitlesPane:'.tab-pane[data-track-kind="subtitles"]',trackCaptionsPane:'.tab-pane[data-track-kind="captions"]',trackDescriptionsPane:'.tab-pane[data-track-kind="descriptions"]',trackChaptersPane:'.tab-pane[data-track-kind="chapters"]',trackMetadataPane:'.tab-pane[data-track-kind="metadata"]'},mediaTypes:{link:"LINK",video:"VIDEO",audio:"AUDIO"},trackKinds:{subtitles:"SUBTITLES",captions:"CAPTIONS",descriptions:"DESCRIPTIONS",chapters:"CHAPTERS",metadata:"METADATA"}},IFRAME:{actions:{remove:'[data-action="remove"]'},elements:{form:"form.tiny_iframecms_form",url:".tiny_iframecms_url",urlWarning:".tiny_iframecms_url_warning",showTitle:".tiny_iframecms_showtitle",linkTitle:".tiny_iframecms_linktitle",showRelated:".tiny_iframecms_showrelated",showUserAvatar:".tiny_iframecms_showuseravatar",responsive:".tiny_iframecms_responsive",startAt:".tiny_iframecms_startat",startAtEnabled:".tiny_iframecms_startat_enabled",aspectRatio:".tiny_iframecms_aspectratio",width:".tiny_iframecms_width",height:".tiny_iframecms_height",preview:".tiny_iframecms_preview",previewContainer:".tiny_iframecms_preview_container",tabs:".tiny_iframecms_tabs",tabUrlBtn:".tiny_iframecms_tab_url_btn",tabIframeLibraryBtn:".tiny_iframecms_tab_iframe_library_btn",paneUrl:".tiny_iframecms_pane_url",paneIframeLibrary:".tiny_iframecms_pane_iframe_library",iframeLibraryContainer:".tiny_iframecms_iframe_library_container",iframeLibraryPlaceholder:".tiny_iframecms_iframe_library_placeholder",iframeLibraryLoading:".tiny_iframecms_iframe_library_loading",iframeLibraryFrame:".tiny_iframecms_iframe_library_frame"},aspectRatios:{"16:9":{width:560,height:315},"4:3":{width:560,height:420},"1:1":{width:400,height:400},custom:null}}},_exports.default}));
//# sourceMappingURL=selectors.min.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,10 @@
define("tiny_mediacms/usedfiles",["exports","core/templates","core/config"],(function(_exports,Templates,_config){var obj;function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,Templates=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}
/**
* Tiny Media Manager usedfiles.
*
* @module tiny_mediacms/usedfiles
* @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/(Templates),_config=(obj=_config)&&obj.__esModule?obj:{default:obj};class UsedFileManager{constructor(files,userContext,itemId,elementId){this.files=files,this.userContext=userContext,this.itemId=itemId,this.elementId=elementId}getElementId(){return this.elementId}getUsedFiles(){const editor=window.parent.tinymce.EditorManager.get(this.getElementId());if(!editor)return window.console.error("Editor not found for ".concat(this.getElementId())),[];const content=editor.getContent(),baseUrl="".concat(_config.default.wwwroot,"/draftfile.php/").concat(this.userContext,"/user/draft/").concat(this.itemId,"/"),pattern=new RegExp("[\"']"+baseUrl.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&")+"(?<filename>.+?)[\\?\"']","gm");return[...content.matchAll(pattern)].map((match=>decodeURIComponent(match.groups.filename)))}findUnusedFiles(usedFiles){return Object.entries(this.files).filter((_ref=>{let[filename]=_ref;return!usedFiles.includes(filename)})).map((_ref2=>{let[filename]=_ref2;return filename}))}findMissingFiles(usedFiles){return usedFiles.filter((filename=>!this.files.hasOwnProperty(filename)))}updateFiles(){const form=document.querySelector("form"),usedFiles=this.getUsedFiles(),unusedFiles=this.findUnusedFiles(usedFiles),missingFiles=this.findMissingFiles(usedFiles);return form.querySelectorAll('input[type=checkbox][name^="deletefile"]').forEach((checkbox=>{unusedFiles.includes(checkbox.dataset.filename)||checkbox.closest(".fitem").remove()})),form.classList.toggle("has-missing-files",!!missingFiles.length),form.classList.toggle("has-unused-files",!!unusedFiles.length),Templates.renderForPromise("tiny_mediacms/missingfiles",{missingFiles:missingFiles}).then((_ref3=>{let{html:html,js:js}=_ref3;Templates.replaceNodeContents(form.querySelector(".missing-files"),html,js)}))}}_exports.init=(files,usercontext,itemid,elementid)=>{const manager=new UsedFileManager(files,usercontext,itemid,elementid);return manager.updateFiles(),manager}}));
//# sourceMappingURL=usedfiles.min.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,265 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Tiny MediaCMS Auto-convert module.
*
* This module automatically converts pasted MediaCMS URLs into embedded videos.
* When a user pastes a MediaCMS video URL (e.g., https://deic.mediacms.io/view?m=JpBd1Zvdl),
* it will be automatically converted to an iframe embed.
*
* @module tiny_mediacms/autoconvert
* @copyright 2024
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {getData} from './options';
/**
* Regular expression patterns for MediaCMS URLs.
* Matches URLs like:
* - https://deic.mediacms.io/view?m=JpBd1Zvdl
* - https://example.mediacms.io/view?m=VIDEO_ID
* - Custom domains configured in the plugin
*/
const MEDIACMS_VIEW_URL_PATTERN = /^(https?:\/\/[^\/]+)\/view\?m=([a-zA-Z0-9_-]+)$/;
/**
* Check if a string is a valid MediaCMS view URL.
*
* @param {string} text - The text to check
* @returns {Object|null} - Parsed URL info or null if not a valid MediaCMS URL
*/
const parseMediaCMSUrl = (text) => {
if (!text || typeof text !== 'string') {
return null;
}
const trimmed = text.trim();
// Check for MediaCMS view URL pattern
const match = trimmed.match(MEDIACMS_VIEW_URL_PATTERN);
if (match) {
return {
baseUrl: match[1],
videoId: match[2],
originalUrl: trimmed,
};
}
return null;
};
/**
* Check if the pasted URL's domain is allowed based on configuration.
*
* @param {Object} parsed - Parsed URL info
* @param {Object} config - Plugin configuration
* @returns {boolean} - True if the domain is allowed
*/
const isDomainAllowed = (parsed, config) => {
// If no specific base URL is configured, allow all MediaCMS domains
const configuredBaseUrl = config.autoConvertBaseUrl || config.mediacmsBaseUrl;
if (!configuredBaseUrl) {
return true;
}
// Check if the URL's base matches the configured base URL
try {
const configuredUrl = new URL(configuredBaseUrl);
const pastedUrl = new URL(parsed.baseUrl);
return configuredUrl.host === pastedUrl.host;
} catch (e) {
// If URL parsing fails, allow the conversion
return true;
}
};
/**
* Generate the iframe embed HTML for a MediaCMS video.
*
* @param {Object} parsed - Parsed URL info
* @param {Object} options - Embed options
* @returns {string} - The iframe HTML
*/
const generateEmbedHtml = (parsed, options = {}) => {
// Build the embed URL with default options
const embedUrl = new URL(`${parsed.baseUrl}/embed`);
embedUrl.searchParams.set('m', parsed.videoId);
// Apply default options (all enabled by default for best user experience)
embedUrl.searchParams.set('showTitle', options.showTitle !== false ? '1' : '0');
embedUrl.searchParams.set('showRelated', options.showRelated !== false ? '1' : '0');
embedUrl.searchParams.set('showUserAvatar', options.showUserAvatar !== false ? '1' : '0');
embedUrl.searchParams.set('linkTitle', options.linkTitle !== false ? '1' : '0');
// Generate clean iframe HTML (wrapper will be added by editor for UI, then stripped on save)
const html = `<iframe ` +
`width="400" height="300" ` +
`style="display: block; border: 0;" ` +
`src="${embedUrl.toString()}" ` +
`allowfullscreen="allowfullscreen">` +
`</iframe>`;
return html;
};
/**
* Set up auto-conversion for the editor.
* This registers event handlers to detect pasted MediaCMS URLs.
*
* @param {TinyMCE} editor - The TinyMCE editor instance
*/
export const setupAutoConvert = (editor) => {
const config = getData(editor) || {};
// Check if auto-convert is enabled (default: true)
if (config.autoConvertEnabled === false) {
return;
}
// Handle paste events
editor.on('paste', (e) => {
handlePasteEvent(editor, e, config);
});
// Also handle input events for drag-and-drop text or keyboard paste
editor.on('input', (e) => {
handleInputEvent(editor, e, config);
});
};
/**
* Handle paste events to detect and convert MediaCMS URLs.
*
* @param {TinyMCE} editor - The TinyMCE editor instance
* @param {Event} e - The paste event
* @param {Object} config - Plugin configuration
*/
const handlePasteEvent = (editor, e, config) => {
// Get pasted text from clipboard
const clipboardData = e.clipboardData || window.clipboardData;
if (!clipboardData) {
return;
}
// Try to get plain text first
const text = clipboardData.getData('text/plain') || clipboardData.getData('text');
if (!text) {
return;
}
// Check if it's a MediaCMS URL
const parsed = parseMediaCMSUrl(text);
if (!parsed) {
return;
}
// Check if domain is allowed
if (!isDomainAllowed(parsed, config)) {
return;
}
// Prevent default paste behavior
e.preventDefault();
e.stopPropagation();
// Generate and insert the embed HTML
const embedHtml = generateEmbedHtml(parsed, config.autoConvertOptions || {});
// Use a slight delay to ensure the editor is ready
setTimeout(() => {
editor.insertContent(embedHtml);
// Move cursor after the inserted content
editor.selection.collapse(false);
}, 0);
};
/**
* Handle input events to catch URLs that might have been pasted without triggering paste event.
* This is a fallback for certain browsers/scenarios.
*
* @param {TinyMCE} editor - The TinyMCE editor instance
* @param {Event} e - The input event
* @param {Object} config - Plugin configuration
*/
const handleInputEvent = (editor, e, config) => {
// Only process inputType 'insertFromPaste' if paste event didn't catch it
if (e.inputType !== 'insertFromPaste' && e.inputType !== 'insertText') {
return;
}
// Get the current node and check if it contains just a URL
const node = editor.selection.getNode();
if (!node || node.nodeName !== 'P') {
return;
}
// Check if the paragraph contains only a MediaCMS URL
const text = node.textContent || '';
const parsed = parseMediaCMSUrl(text);
if (!parsed || !isDomainAllowed(parsed, config)) {
return;
}
// Don't convert if there's other content in the paragraph
const trimmedHtml = node.innerHTML.trim();
if (trimmedHtml !== text.trim() && !trimmedHtml.startsWith(text.trim())) {
return;
}
// Generate the embed HTML
const embedHtml = generateEmbedHtml(parsed, config.autoConvertOptions || {});
// Replace the paragraph content with the embed
// Use a slight delay to let the input event complete
setTimeout(() => {
// Re-check that the node still contains the URL (user might have typed more)
const currentText = node.textContent || '';
const currentParsed = parseMediaCMSUrl(currentText);
if (currentParsed && currentParsed.originalUrl === parsed.originalUrl) {
// Select and replace the entire node
editor.selection.select(node);
editor.insertContent(embedHtml);
}
}, 100);
};
/**
* Check if a text is a MediaCMS URL (public helper).
*
* @param {string} text - The text to check
* @returns {boolean} - True if it's a MediaCMS URL
*/
export const isMediaCMSUrl = (text) => {
return parseMediaCMSUrl(text) !== null;
};
/**
* Convert a MediaCMS URL to embed HTML (public helper).
*
* @param {string} url - The MediaCMS URL
* @param {Object} options - Embed options
* @returns {string|null} - The embed HTML or null if not a valid URL
*/
export const convertToEmbed = (url, options = {}) => {
const parsed = parseMediaCMSUrl(url);
if (!parsed) {
return null;
}
return generateEmbedHtml(parsed, options);
};

Some files were not shown because too many files have changed in this diff Show More