This commit is contained in:
Markos Gogoulos
2026-02-03 19:23:02 +02:00
parent c4d569e7b0
commit e12f361935
89 changed files with 8689 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,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>

After

Width:  |  Height:  |  Size: 238 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
<circle cx="50" cy="50" r="48" fill="#2EAF5A"/>
<polygon points="38,28 38,72 75,50" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 200 B

View File

@@ -0,0 +1,97 @@
<?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/>.
/**
* Settings for the tiny_mediacms plugin.
*
* @package tiny_mediacms
* @copyright 2024
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
if ($ADMIN->fulltree) {
global $DB;
// LTI Tool ID setting (Dropdown).
$ltioptions = [0 => get_string('noltitoolsfound', 'tiny_mediacms')];
try {
$tools = $DB->get_records('lti_types', null, 'name ASC', 'id, name, baseurl');
if (!empty($tools)) {
$ltioptions = [0 => get_string('choose', 'tiny_mediacms')];
foreach ($tools as $tool) {
$ltioptions[$tool->id] = $tool->name . ' (' . $tool->baseurl . ')';
}
}
} catch (Exception $e) {
// Database might not be ready during install
}
$setting = new admin_setting_configselect(
'tiny_mediacms/ltitoolid',
new lang_string('ltitoolid', 'tiny_mediacms'),
new lang_string('ltitoolid_desc', 'tiny_mediacms'),
0,
$ltioptions
);
$settings->add($setting);
// Auto-convert is enabled by default in plugininfo.php (data.autoConvertEnabled = true).
// MediaCMS base URL for auto-convert.
$setting = new admin_setting_configtext(
'tiny_mediacms/autoconvert_baseurl',
new lang_string('autoconvert_baseurl', 'tiny_mediacms'),
new lang_string('autoconvert_baseurl_desc', 'tiny_mediacms'),
'https://lti.mediacms.io', // Default matching filter
PARAM_URL
);
$settings->add($setting);
// Auto-convert embed options.
$setting = new admin_setting_configcheckbox(
'tiny_mediacms/autoconvert_showtitle',
new lang_string('autoconvert_showtitle', 'tiny_mediacms'),
new lang_string('autoconvert_showtitle_desc', 'tiny_mediacms'),
1
);
$settings->add($setting);
$setting = new admin_setting_configcheckbox(
'tiny_mediacms/autoconvert_linktitle',
new lang_string('autoconvert_linktitle', 'tiny_mediacms'),
new lang_string('autoconvert_linktitle_desc', 'tiny_mediacms'),
1
);
$settings->add($setting);
$setting = new admin_setting_configcheckbox(
'tiny_mediacms/autoconvert_showrelated',
new lang_string('autoconvert_showrelated', 'tiny_mediacms'),
new lang_string('autoconvert_showrelated_desc', 'tiny_mediacms'),
1
);
$settings->add($setting);
$setting = new admin_setting_configcheckbox(
'tiny_mediacms/autoconvert_showuseravatar',
new lang_string('autoconvert_showuseravatar', 'tiny_mediacms'),
new lang_string('autoconvert_showuseravatar_desc', 'tiny_mediacms'),
1
);
$settings->add($setting);
}

View File

@@ -0,0 +1,83 @@
#tiny_mediacms_form {
padding: 1rem;
}
#tiny_mediacms_form #id_deletefileshdr {
display: none;
}
#tiny_mediacms_form.has-unused-files #id_deletefileshdr {
display: block;
}
#tiny_mediacms_form #id_missingfileshdr {
display: none;
}
#tiny_mediacms_form.has-missing-files #id_missingfileshdr {
display: block;
}
iframe.mmcms_iframe {
height: 650px;
border: none;
width: 100%;
}
.missing-files ol {
padding-left: 15px;
}
.missing-files ol li {
font-style: italic;
font-weight: 600;
color: red;
}
.tiny_imagecms_form .tiny_imagecms_dropzone_container {
height: 200px;
}
.tiny_imagecms_form .tiny_imagecms_dropzone_container .dropzone-label {
font-size: 1.25rem;
}
.tiny_imagecms_form .tiny_imagecms_loader_container {
height: 200px;
}
.tiny_imagecms_form .tiny_imagecms_preview_box {
height: 300px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.tiny_imagecms_form .tiny_imagecms_deleteicon {
position: absolute;
top: 5px;
right: 5px;
cursor: pointer;
z-index: 1;
width: 30px;
height: 30px;
background: rgba(255, 255, 255, 1);
border-radius: 50%;
padding: 4px 5px 5px 9px;
}
.tiny_imagecms_form .tiny_imagecms_deleteicon .fa-trash {
color: #1d2125;
}
label.form-check-label {
margin-left: 8px;
}
@media (max-width: 767px) {
.tiny_imagecms_form .tiny_imagecms_properties_col {
padding: 0;
}
}

View File

@@ -0,0 +1,41 @@
{{!
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/>.
}}
{{!
@template tiny_mediacms/embed_media_audio
Embed media audio template.
Example context (json):
{
}
}}
&nbsp;<audio{{!
}}{{#showControls}} controls="true" {{/showControls}}{{!
}}{{#loop}} loop="true"{{/loop}}{{!
}}{{#muted}} muted="true"{{/muted}}{{!
}}{{#autoplay}} autoplay="true"{{/autoplay}}{{!
}}{{#title}} title="{{.}}"{{/title}}{{!
}}>
{{#sources}}<source src="{{.}}"/>{{/sources}}
{{#tracks}}
<track src="{{track}}" kind="{{kind}}" srclang="{{srclang}}" label="{{label}}"{{!
}}{{#defaultTrack}} default="true"{{/defaultTrack}}{{!
}}>
{{/tracks}}
{{#description}}{{.}}{{/description}}
</audio>&nbsp;

View File

@@ -0,0 +1,33 @@
{{!
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/>.
}}
{{!
@template tiny_mediacms/embed_media_link
Embed media link template.
Example context (json):
{
}
}}
<a href="{{url}}"{{!
}}{{#width}} data-width="{{.}}"{{/width}}{{!
}}{{#height}} data-height="{{.}}"{{/height}}{{!
}}>{{!
}}{{#name}}{{.}}{{/name}}{{!
}}{{^name}}{{url}}{{/name}}{{!
}}</a>

View File

@@ -0,0 +1,78 @@
{{!
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/>.
}}
{{!
@template tiny_mediacms/embed_media_modal
Embed media modal template.
Example context (json):
{
}
}}
{{< core/modal }}
{{$title}}
{{#str}} modaltitle, tiny_h5p {{/str}}
{{/title}}
{{$body}}
<form class="tiny_mediacms_form" id="{{elementid}}_tiny_mediacms_form">
<ul class="root nav nav-tabs mb-1" role="tablist">
<li data-medium-type="link" class="nav-item">
<a class="nav-link {{# link }}active{{/ link }}" href="#{{elementid}}_link" role="tab" data-toggle="tab">
{{#str}} link, tiny_mediacms {{/str}}
</a>
</li>
<li data-medium-type="video" class="nav-item">
<a class="nav-link {{# video }}active{{/ video }}" href="#{{elementid}}_video" role="tab" data-toggle="tab">
{{#str}} video, tiny_mediacms {{/str}}
</a>
</li>
<li data-medium-type="audio" class="nav-item">
<a class="nav-link {{# audio }}active{{/ audio }}" href="#{{elementid}}_audio" role="tab" data-toggle="tab">
{{#str}} audio, tiny_mediacms {{/str}}
</a>
</li>
</ul>
<div class="root tab-content">
<div data-medium-type="link" class="tab-pane {{# link }}active{{/ link }}" id="{{elementid}}_link">
{{> tiny_mediacms/embed_media_modal_link }}
</div>
<div data-medium-type="video" class="tab-pane {{# video }}active{{/ video }}" id="{{elementid}}_video">
{{> tiny_mediacms/embed_media_modal_video}}
</div>
<div data-medium-type="audio" class="tab-pane {{# audio }}active{{/ audio }}" id="{{elementid}}_audio">
{{> tiny_mediacms/embed_media_modal_audio}}
</div>
</div>
</form>
{{/body}}
{{$footer}}
<button type="button" class="btn btn-primary" data-action="save">
{{#isupdating}}
{{#str}} updatemedia, tiny_mediacms {{/str}}
{{/isupdating}}
{{^isupdating}}
{{#str}} createmedia, tiny_mediacms {{/str}}
{{/isupdating}}
</button>
<button type="button" class="btn btn-secondary" data-action="cancel">{{#str}} cancel, moodle {{/str}}</button>
{{/footer}}
{{/ core/modal }}

View File

@@ -0,0 +1,802 @@
{{!
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/>.
}}
{{!
@template tiny_mediacms/embed_media_modal_audio
Embed media audio modal template.
Example context (json):
{
}
}}
{{#audio.sources}}
<div class="tiny_mediacms_source tiny_mediacms_media_source">
<div class="mb-1">
<label for="audio-audio-url-input">
{{#str}} audiosourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="audio-audio-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32" value="{{.}}"/>
{{#showfilepicker}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepicker}}
</div>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} addsource, tiny_mediacms {{/str}}
</a>
{{#addsourcehelpicon}}
{{> core/help_icon }}
{{/addsourcehelpicon}}
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/audio.sources}}
{{^audio}}
<div class="tiny_mediacms_source tiny_mediacms_media_source">
<div class="mb-1">
<label for="audio-audio-url-input">
{{#str}} audiosourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="audio-audio-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32"/>
{{#showfilepicker}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepicker}}
</div>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} addsource, tiny_mediacms {{/str}}
</a>
{{#addsourcehelpicon}}
{{> core/help_icon }}
{{/addsourcehelpicon}}
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/audio}}
<fieldset class="collapsible collapsed" id="{{elementid}}_audio-display-options">
<input name="mform_isexpanded_{{elementid}}_audio-display-options" type="hidden">
<legend class="d-flex align-items-center px-1">
<div class="position-relative d-flex ftoggler align-items-center position-relative me-1">
<a role="button" data-toggle="collapse" href="#adisplayoptions" aria-expanded="false"
aria-controls="adisplayoptions"
class="btn btn-icon me-3 icons-collapse-expand stretched-link fheader collapsed">
<span class="expanded-icon icon-no-margin p-2" title="{{#str}} collapse, moodle {{/str}}">
<i class="icon fa fa-chevron-down fa-fw " aria-hidden="true"></i>
</span>
<span class="collapsed-icon icon-no-margin p-2" title="{{#str}} expand, moodle {{/str}}">
<span class="dir-rtl-hide">
<i class="icon fa fa-chevron-right fa-fw " aria-hidden="true"></i>
</span>
<span class="dir-ltr-hide">
<i class="icon fa fa-chevron-left fa-fw " aria-hidden="true"></i>
</span>
</span>
<span class="sr-only">{{#str}} displayoptions, tiny_mediacms {{/str}}</span>
</a>
<h3 class="d-flex align-self-stretch align-items-center mb-0" aria-hidden="true">
{{#str}} displayoptions, tiny_mediacms {{/str}}
</h3>
</div>
</legend>
<div id="adisplayoptions" class="fcontainer collapseable collapse px-1">
<div class="tiny_mediacms_display_options">
<div class="mb-1">
<label for="adisplayoptions_media-title-entry">{{#str}} entertitle, tiny_mediacms {{/str}}</label>
<input class="form-control fullwidth tiny_mediacms_title_entry" type="text" id="adisplayoptions_media-title-entry"
size="32" value="{{audio.title}}"/>
</div>
</div>
</div>
</fieldset>
<fieldset class="collapsible collapsed" id="{{elementid}}_audio-advanced-settings">
<input name="mform_isexpanded_{{elementid}}_audio-advanced-settings" type="hidden">
<legend class="d-flex align-items-center px-1">
<div class="position-relative d-flex ftoggler align-items-center position-relative me-1">
<a role="button" data-toggle="collapse" href="#aadvancedsettings" aria-expanded="false"
aria-controls="aadvancedsettings"
class="btn btn-icon me-3 icons-collapse-expand stretched-link fheader collapsed">
<span class="expanded-icon icon-no-margin p-2" title="{{#str}} collapse, moodle {{/str}}">
<i class="icon fa fa-chevron-down fa-fw " aria-hidden="true"></i>
</span>
<span class="collapsed-icon icon-no-margin p-2" title="{{#str}} expand, moodle {{/str}}">
<span class="dir-rtl-hide">
<i class="icon fa fa-chevron-right fa-fw " aria-hidden="true"></i>
</span>
<span class="dir-ltr-hide">
<i class="icon fa fa-chevron-left fa-fw " aria-hidden="true"></i>
</span>
</span>
<span class="sr-only">{{#str}} advancedsettings, tiny_mediacms {{/str}}</span>
</a>
<h3 class="d-flex align-self-stretch align-items-center mb-0" aria-hidden="true">
{{#str}} advancedsettings, tiny_mediacms {{/str}}
</h3>
</div>
</legend>
<div id="aadvancedsettings" class="fcontainer collapseable collapse px-1">
<div class="tiny_mediacms_advancedsettings">
<div class="form-check">
<input type="checkbox" checked="true" class="form-check-input tiny_mediacms_controls"
id="aadvancedsettings_media-controls-toggle" {{# audio.controls }}checked{{/ audio.controls }}/>
<label class="form-check-label" for="aadvancedsettings_media-controls-toggle">
{{#str}} controls, tiny_mediacms {{/str}}
</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_autoplay"
id="aadvancedsettings_media-autoplay-toggle" {{# audio.autoplay }}checked{{/ audio.autoplay }}/>
<label class="form-check-label" for="aadvancedsettings_media-autoplay-toggle">
{{#str}} autoplay, tiny_mediacms {{/str}}
</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_mute"
id="aadvancedsettings_media-mute-toggle" {{# audio.muted }}checked{{/ audio.muted }}/>
<label class="form-check-label" for="aadvancedsettings_media-mute-toggle">
{{#str}} mute, tiny_mediacms {{/str}}
</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_loop"
id="aadvancedsettings_media-loop-toggle" {{# audio.loop }}checked{{/ audio.loop }}/>
<label class="form-check-label" for="aadvancedsettings_media-loop-toggle">
{{#str}} loop, tiny_mediacms {{/str}}
</label>
</div>
</div>
</div>
</fieldset>
<fieldset class="collapsible collapsed" id="{{elementid}}_audio-tracks">
<input name="mform_isexpanded_{{elementid}}_audio-tracks" type="hidden">
<legend class="d-flex align-items-center px-1">
<div class="position-relative d-flex ftoggler align-items-center position-relative me-1">
<a role="button" data-toggle="collapse" href="#atracks" aria-expanded="false"
aria-controls="atracks"
class="btn btn-icon me-3 icons-collapse-expand stretched-link fheader collapsed">
<span class="expanded-icon icon-no-margin p-2" title="{{#str}} collapse, moodle {{/str}}">
<i class="icon fa fa-chevron-down fa-fw " aria-hidden="true"></i>
</span>
<span class="collapsed-icon icon-no-margin p-2" title="{{#str}} expand, moodle {{/str}}">
<span class="dir-rtl-hide">
<i class="icon fa fa-chevron-right fa-fw " aria-hidden="true"></i>
</span>
<span class="dir-ltr-hide">
<i class="icon fa fa-chevron-left fa-fw " aria-hidden="true"></i>
</span>
</span>
<span class="sr-only">{{#str}} tracks, tiny_mediacms {{/str}}</span>
</a>
<h3 class="d-flex align-self-stretch align-items-center mb-0" aria-hidden="true">
{{#str}} tracks, tiny_mediacms {{/str}}
</h3>
</div>
{{#trackshelpicon}}
{{> core/help_icon }}
{{/trackshelpicon}}
</legend>
<div id="atracks" class="fcontainer collapseable collapse px-1">
<ul class="nav nav-tabs mb-3">
<li data-track-kind="subtitles" class="nav-item">
<a class="nav-link active" href="#{{elementid}}_atracks_subtitles"
role="tab" data-toggle="tab">
{{#str}} subtitles, tiny_mediacms {{/str}}
</a>
</li>
<li data-track-kind="captions" class="nav-item">
<a class="nav-link" href="#{{elementid}}_atracks_captions" role="tab" data-toggle="tab">
{{#str}} captions, tiny_mediacms {{/str}}
</a>
</li>
<li data-track-kind="descriptions" class="nav-item">
<a class="nav-link" href="#{{elementid}}_atracks_descriptions"
role="tab" data-toggle="tab">
{{#str}} descriptions, tiny_mediacms {{/str}}
</a>
</li>
<li data-track-kind="chapters" class="nav-item">
<a class="nav-link" href="#{{elementid}}_atracks_chapters" role="tab" data-toggle="tab">
{{#str}} chapters, tiny_mediacms {{/str}}
</a>
</li>
<li data-track-kind="metadata" class="nav-item">
<a class="nav-link" href="#{{elementid}}_atracks_metadata" role="tab" data-toggle="tab">
{{#str}} metadata, tiny_mediacms {{/str}}
</a>
</li>
</ul>
<div class="tab-content">
<div data-track-kind="subtitles" class="tab-pane active"
id="{{elementid}}_atracks_subtitles">
<div class="trackhelp">
{{#subtitleshelpicon}}
{{> core/help_icon }}
{{/subtitleshelpicon}}
</div>
{{#audio.tracks.subtitles}}
<div class="mb-1 tiny_mediacms_track">
<div class="tiny_mediacms_source tiny_mediacms_track_source">
<div class="mb-1">
<label for="subtitle-audio-url-input">
{{#str}} subtitlessourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="subtitle-audio-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32" value="{{src}}"/>
{{#showfilepickertrack}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepickertrack}}
</div>
</div>
</div>
<div class="mb-3">
<label class="w-100" for="subtitle-audio-lang-input">{{#str}} srclang, tiny_mediacms {{/str}}</label>
<select id="subtitle-audio-lang-input" class="custom-select tiny_mediacms_track_lang_entry" data-value="{{srclang}}">
<optgroup label="{{#str}} languagesinstalled, tiny_mediacms {{/str}}">
{{#langsinstalled}}
<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>
{{/langsinstalled}}
</optgroup>
<optgroup label="{{#str}} languagesavailable, tiny_mediacms {{/str}} ">
{{#langsavailable}}
<option value="{{code}}">{{lang}}</option>
{{/langsavailable}}
</optgroup>
</select>
</div>
<div class="mb-3">
<label class="w-100" for="subtitle-audio-track-input">{{#str}} label, tiny_mediacms {{/str}}</label>
<input id="subtitle-audio-track-input" class="form-control tiny_mediacms_track_label_entry" type="text" value="{{label}}"/>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_track_default" {{# defaultTrack }}checked{{/ defaultTrack }}/>
<label class="form-check-label">{{#str}} default, tiny_mediacms {{/str}}</label>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} addsubtitlestrack, tiny_mediacms {{/str}}
</a>
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/audio.tracks.subtitles}}
{{^audio.tracks.subtitles}}
<div class="mb-1 tiny_mediacms_track">
<div class="tiny_mediacms_source tiny_mediacms_track_source">
<div class="mb-1">
<label for="subtitle-audio-url-input">
{{#str}} subtitlessourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="subtitle-audio-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32"/>
{{#showfilepickertrack}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepickertrack}}
</div>
</div>
</div>
<div class="mb-3">
<label class="w-100" for="subtitle-audio-lang-input">{{#str}} srclang, tiny_mediacms {{/str}}</label>
<select id="subtitle-audio-lang-input" class="custom-select tiny_mediacms_track_lang_entry">
<optgroup label="{{#str}} languagesinstalled, tiny_mediacms {{/str}}">
{{#langsinstalled}}
<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>
{{/langsinstalled}}
</optgroup>
<optgroup label="{{#str}} languagesavailable, tiny_mediacms {{/str}} ">
{{#langsavailable}}
<option value="{{code}}">{{lang}}</option>
{{/langsavailable}}
</optgroup>
</select>
</div>
<div class="mb-3">
<label class="w-100" for="subtitle-audio-track-input">{{#str}} label, tiny_mediacms {{/str}}</label>
<input id="subtitle-audio-track-input" class="form-control tiny_mediacms_track_label_entry" type="text"/>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_track_default"/>
<label class="form-check-label">{{#str}} default, tiny_mediacms {{/str}}</label>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} addsubtitlestrack, tiny_mediacms {{/str}}
</a>
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/audio.tracks.subtitles}}
</div>
<div data-track-kind="captions" class="tab-pane"
id="{{elementid}}_atracks_captions">
<div class="trackhelp">
{{#captionshelpicon}}
{{> core/help_icon }}
{{/captionshelpicon}}
</div>
{{#audio.tracks.captions}}
<div class="mb-1 tiny_mediacms_track">
<div class="tiny_mediacms_source tiny_mediacms_track_source">
<div class="mb-1">
<label for="caption-audio-url-input">
{{#str}} captionssourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="caption-audio-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32" value="{{src}}"/>
{{#showfilepickertrack}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepickertrack}}
</div>
</div>
</div>
<div class="mb-3">
<label class="w-100" for="caption-audio-lang-input">{{#str}} srclang, tiny_mediacms {{/str}}</label>
<select id="caption-audio-lang-input" class="custom-select tiny_mediacms_track_lang_entry" data-value="{{srclang}}">
<optgroup label="{{#str}} languagesinstalled, tiny_mediacms {{/str}}">
{{#langsinstalled}}
<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>
{{/langsinstalled}}
</optgroup>
<optgroup label="{{#str}} languagesavailable, tiny_mediacms {{/str}} ">
{{#langsavailable}}
<option value="{{code}}">{{lang}}</option>
{{/langsavailable}}
</optgroup>
</select>
</div>
<div class="mb-3">
<label class="w-100" for="caption-audio-track-input">{{#str}} label, tiny_mediacms {{/str}}</label>
<input id="caption-audio-track-input" class="form-control tiny_mediacms_track_label_entry" type="text" value="{{label}}"/>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_track_default" {{# defaultTrack }}checked{{/ defaultTrack }}/>
<label class="form-check-label">{{#str}} default, tiny_mediacms {{/str}}</label>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} addcaptionstrack, tiny_mediacms {{/str}}
</a>
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/audio.tracks.captions}}
{{^audio.tracks.captions}}
<div class="mb-1 tiny_mediacms_track">
<div class="tiny_mediacms_source tiny_mediacms_track_source">
<div class="mb-1">
<label for="caption-audio-url-input">
{{#str}} captionssourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="caption-audio-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32"/>
{{#showfilepickertrack}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepickertrack}}
</div>
</div>
</div>
<div class="mb-3">
<label class="w-100" for="caption-audio-lang-input">{{#str}} srclang, tiny_mediacms {{/str}}</label>
<select id="caption-audio-lang-input" class="custom-select tiny_mediacms_track_lang_entry">
<optgroup label="{{#str}} languagesinstalled, tiny_mediacms {{/str}}">
{{#langsinstalled}}
<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>
{{/langsinstalled}}
</optgroup>
<optgroup label="{{#str}} languagesavailable, tiny_mediacms {{/str}} ">
{{#langsavailable}}
<option value="{{code}}">{{lang}}</option>
{{/langsavailable}}
</optgroup>
</select>
</div>
<div class="mb-3">
<label class="w-100" for="caption-audio-track-input">{{#str}} label, tiny_mediacms {{/str}}</label>
<input id="caption-audio-track-input" class="form-control tiny_mediacms_track_label_entry" type="text"/>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_track_default"/>
<label class="form-check-label">{{#str}} default, tiny_mediacms {{/str}}</label>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} addcaptionstrack, tiny_mediacms {{/str}}
</a>
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/audio.tracks.captions}}
</div>
<div data-track-kind="descriptions" class="tab-pane"
id="{{elementid}}_atracks_descriptions">
<div class="trackhelp">
{{#descriptionshelpicon}}
{{> core/help_icon }}
{{/descriptionshelpicon}}
</div>
{{#audio.tracks.descriptions}}
<div class="mb-1 tiny_mediacms_track">
<div class="tiny_mediacms_source tiny_mediacms_track_source">
<div class="mb-1">
<label for="description-audio-url-input">
{{#str}} descriptionssourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="description-audio-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32" value="{{src}}"/>
{{#showfilepickertrack}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepickertrack}}
</div>
</div>
</div>
<div class="mb-3">
<label class="w-100" for="description-audio-lang-input">{{#str}} srclang, tiny_mediacms {{/str}}</label>
<select id="description-audio-lang-input" class="custom-select tiny_mediacms_track_lang_entry" data-value="{{srclang}}">
<optgroup label="{{#str}} languagesinstalled, tiny_mediacms {{/str}}">
{{#langsinstalled}}
<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>
{{/langsinstalled}}
</optgroup>
<optgroup label="{{#str}} languagesavailable, tiny_mediacms {{/str}} ">
{{#langsavailable}}
<option value="{{code}}">{{lang}}</option>
{{/langsavailable}}
</optgroup>
</select>
</div>
<div class="mb-3">
<label class="w-100" for="description-audio-track-input">{{#str}} label, tiny_mediacms {{/str}}</label>
<input id="description-audio-track-input" class="form-control tiny_mediacms_track_label_entry" type="text" value="{{label}}"/>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_track_default" {{# defaultTrack }}checked{{/ defaultTrack }}/>
<label class="form-check-label">{{#str}} default, tiny_mediacms {{/str}}</label>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} adddescriptionstrack, tiny_mediacms {{/str}}
</a>
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/audio.tracks.descriptions}}
{{^audio.tracks.descriptions}}
<div class="mb-1 tiny_mediacms_track">
<div class="tiny_mediacms_source tiny_mediacms_track_source">
<div class="mb-1">
<label for="description-audio-url-input">
{{#str}} descriptionssourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="description-audio-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32"/>
{{#showfilepickertrack}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepickertrack}}
</div>
</div>
</div>
<div class="mb-3">
<label class="w-100" for="description-audio-lang-input">{{#str}} srclang, tiny_mediacms {{/str}}</label>
<select id="description-audio-lang-input" class="custom-select tiny_mediacms_track_lang_entry">
<optgroup label="{{#str}} languagesinstalled, tiny_mediacms {{/str}}">
{{#langsinstalled}}
<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>
{{/langsinstalled}}
</optgroup>
<optgroup label="{{#str}} languagesavailable, tiny_mediacms {{/str}} ">
{{#langsavailable}}
<option value="{{code}}">{{lang}}</option>
{{/langsavailable}}
</optgroup>
</select>
</div>
<div class="mb-3">
<label class="w-100" for="description-audio-track-input">{{#str}} label, tiny_mediacms {{/str}}</label>
<input id="description-audio-track-input" class="form-control tiny_mediacms_track_label_entry" type="text"/>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_track_default"/>
<label class="form-check-label">{{#str}} default, tiny_mediacms {{/str}}</label>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} adddescriptionstrack, tiny_mediacms {{/str}}
</a>
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/audio.tracks.descriptions}}
</div>
<div data-track-kind="chapters" class="tab-pane"
id="{{elementid}}_atracks_chapters">
<div class="trackhelp">
{{#chaptershelpicon}}
{{> core/help_icon }}
{{/chaptershelpicon}}
</div>
{{#audio.tracks.chapters}}
<div class="mb-1 tiny_mediacms_track">
<div class="tiny_mediacms_source tiny_mediacms_track_source">
<div class="mb-1">
<label for="chapter-audio-url-input">
{{#str}} chapterssourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="chapter-audio-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32" value="{{src}}"/>
{{#showfilepickertrack}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepickertrack}}
</div>
</div>
</div>
<div class="mb-3">
<label class="w-100" for="chapter-audio-lang-input">{{#str}} srclang, tiny_mediacms {{/str}}</label>
<select id="chapter-audio-lang-input" class="custom-select tiny_mediacms_track_lang_entry" data-value="{{srclang}}">
<optgroup label="{{#str}} languagesinstalled, tiny_mediacms {{/str}}">
{{#langsinstalled}}
<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>
{{/langsinstalled}}
</optgroup>
<optgroup label="{{#str}} languagesavailable, tiny_mediacms {{/str}} ">
{{#langsavailable}}
<option value="{{code}}">{{lang}}</option>
{{/langsavailable}}
</optgroup>
</select>
</div>
<div class="mb-3">
<label class="w-100" for="chapter-audio-track-input">{{#str}} label, tiny_mediacms {{/str}}</label>
<input id="chapter-audio-track-input" class="form-control tiny_mediacms_track_label_entry" type="text" value="{{label}}"/>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_track_default" {{# defaultTrack }}checked{{/ defaultTrack }}/>
<label class="form-check-label">{{#str}} default, tiny_mediacms {{/str}}</label>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} addchapterstrack, tiny_mediacms {{/str}}
</a>
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/audio.tracks.chapters}}
{{^audio.tracks.chapters}}
<div class="mb-1 tiny_mediacms_track">
<div class="tiny_mediacms_source tiny_mediacms_track_source">
<div class="mb-1">
<label for="chapter-audio-url-input">
{{#str}} chapterssourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="chapter-audio-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32"/>
{{#showfilepickertrack}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepickertrack}}
</div>
</div>
</div>
<div class="mb-3">
<label class="w-100" for="chapter-audio-lang-input">{{#str}} srclang, tiny_mediacms {{/str}}</label>
<select id="chapter-audio-lang-input" class="custom-select tiny_mediacms_track_lang_entry">
<optgroup label="{{#str}} languagesinstalled, tiny_mediacms {{/str}}">
{{#langsinstalled}}
<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>
{{/langsinstalled}}
</optgroup>
<optgroup label="{{#str}} languagesavailable, tiny_mediacms {{/str}} ">
{{#langsavailable}}
<option value="{{code}}">{{lang}}</option>
{{/langsavailable}}
</optgroup>
</select>
</div>
<div class="mb-3">
<label class="w-100" for="chapter-audio-track-input">{{#str}} label, tiny_mediacms {{/str}}</label>
<input id="chapter-audio-track-input" class="form-control tiny_mediacms_track_label_entry" type="text"/>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_track_default"/>
<label class="form-check-label">{{#str}} default, tiny_mediacms {{/str}}</label>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} addchapterstrack, tiny_mediacms {{/str}}
</a>
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/audio.tracks.chapters}}
</div>
<div data-track-kind="metadata" class="tab-pane"
id="{{elementid}}_atracks_metadata">
<div class="trackhelp">{{{helpStrings.metadata}}}</div>
<div class="trackhelp">
{{#metadatahelpicon}}
{{> core/help_icon }}
{{/metadatahelpicon}}
</div>
{{#audio.tracks.metadata}}
<div class="mb-1 tiny_mediacms_track">
<div class="tiny_mediacms_source tiny_mediacms_track_source">
<div class="mb-1">
<label for="metadata-audio-url-input">
{{#str}} metadatasourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="metadata-audio-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32" value="{{src}}"/>
{{#showfilepickertrack}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepickertrack}}
</div>
</div>
</div>
<div class="mb-3">
<label class="w-100" for="metadata-audio-lang-input">{{#str}} srclang, tiny_mediacms {{/str}}</label>
<select id="metadata-audio-lang-input" class="custom-select tiny_mediacms_track_lang_entry" data-value="{{srclang}}">
<optgroup label="{{#str}} languagesinstalled, tiny_mediacms {{/str}}">
{{#langsinstalled}}
<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>
{{/langsinstalled}}
</optgroup>
<optgroup label="{{#str}} languagesavailable, tiny_mediacms {{/str}} ">
{{#langsavailable}}
<option value="{{code}}">{{lang}}</option>
{{/langsavailable}}
</optgroup>
</select>
</div>
<div class="mb-3">
<label class="w-100" for="metadata-audio-track-input">{{#str}} label, tiny_mediacms {{/str}}</label>
<input id="metadata-audio-track-input" class="form-control tiny_mediacms_track_label_entry" type="text" value="{{label}}"/>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_track_default" {{# defaultTrack }}checked{{/ defaultTrack }}/>
<label class="form-check-label">{{#str}} default, tiny_mediacms {{/str}}</label>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} addmetadatatrack, tiny_mediacms {{/str}}
</a>
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/audio.tracks.metadata}}
{{^audio.tracks.metadata}}
<div class="mb-1 tiny_mediacms_track">
<div class="tiny_mediacms_source tiny_mediacms_track_source">
<div class="mb-1">
<label for="metadata-audio-url-input">
{{#str}} metadatasourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="metadata-audio-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32"/>
{{#showfilepickertrack}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepickertrack}}
</div>
</div>
</div>
<div class="mb-3">
<label class="w-100" for="metadata-audio-lang-input">{{#str}} srclang, tiny_mediacms {{/str}}</label>
<select id="metadata-audio-lang-input" class="custom-select tiny_mediacms_track_lang_entry">
<optgroup label="{{#str}} languagesinstalled, tiny_mediacms {{/str}}">
{{#langsinstalled}}
<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>
{{/langsinstalled}}
</optgroup>
<optgroup label="{{#str}} languagesavailable, tiny_mediacms {{/str}} ">
{{#langsavailable}}
<option value="{{code}}">{{lang}}</option>
{{/langsavailable}}
</optgroup>
</select>
</div>
<div class="mb-3">
<label class="w-100" for="metadata-audio-track-input">{{#str}} label, tiny_mediacms {{/str}}</label>
<input id="metadata-audio-track-input" class="form-control tiny_mediacms_track_label_entry" type="text"/>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_track_default"/>
<label class="form-check-label">{{#str}} default, tiny_mediacms {{/str}}</label>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} addmetadatatrack, tiny_mediacms {{/str}}
</a>
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/audio.tracks.metadata}}
</div>
</div>
</div>
</fieldset>

View File

@@ -0,0 +1,43 @@
{{!
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/>.
}}
{{!
@template tiny_mediacms/embed_media_modal_link
Embed media link modal template.
Example context (json):
{
}
}}
<div class="tiny_mediacms_source {{id}}">
<div class="mb-1">
<label for="source-url-input">
{{#str}} entersource, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="source-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32"/>
{{#showfilepicker}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepicker}}
</div>
</div>
</div>
<label for="{{elementid}}_link_nameentry">{{#str}} entername, tiny_mediacms {{/str}}</label>
<input class="form-control fullwidth tiny_mediacms_name_entry" type="text" id="{{elementid}}_link_nameentry" size="32" required="true"/>

View File

@@ -0,0 +1,832 @@
{{!
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/>.
}}
{{!
@template tiny_mediacms/embed_media_modal_video
Embed media video modal template.
Example context (json):
{
}
}}
{{#video.sources}}
<div class="tiny_mediacms_source tiny_mediacms_media_source">
<div class="mb-1">
<label for="video-video-url-input">
{{#str}} videosourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="video-video-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32" value="{{.}}"/>
{{#showfilepicker}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepicker}}
</div>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} addsource, tiny_mediacms {{/str}}
</a>
{{#addsourcehelpicon}}
{{> core/help_icon }}
{{/addsourcehelpicon}}
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/video.sources}}
{{^video}}
<div class="tiny_mediacms_source tiny_mediacms_media_source">
<div class="mb-1">
<label for="video-video-url-input">
{{#str}} videosourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="video-video-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32"/>
{{#showfilepicker}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepicker}}
</div>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} addsource, tiny_mediacms {{/str}}
</a>
{{#addsourcehelpicon}}
{{> core/help_icon }}
{{/addsourcehelpicon}}
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/video}}
<fieldset class="collapsible collapsed" id="{{elementid}}_video-display-options">
<input name="mform_isexpanded_{{elementid}}_video-display-options" type="hidden">
<legend class="d-flex align-items-center px-1">
<div class="position-relative d-flex ftoggler align-items-center position-relative me-1">
<a role="button" data-toggle="collapse" href="#vdisplayoptions" aria-expanded="false"
aria-controls="vdisplayoptions"
class="btn btn-icon me-3 icons-collapse-expand stretched-link fheader collapsed">
<span class="expanded-icon icon-no-margin p-2" title="{{#str}} collapse, moodle {{/str}}">
<i class="icon fa fa-chevron-down fa-fw " aria-hidden="true"></i>
</span>
<span class="collapsed-icon icon-no-margin p-2" title="{{#str}} expand, moodle {{/str}}">
<span class="dir-rtl-hide">
<i class="icon fa fa-chevron-right fa-fw " aria-hidden="true"></i>
</span>
<span class="dir-ltr-hide">
<i class="icon fa fa-chevron-left fa-fw " aria-hidden="true"></i>
</span>
</span>
<span class="sr-only">{{#str}} displayoptions, tiny_mediacms {{/str}}</span>
</a>
<h3 class="d-flex align-self-stretch align-items-center mb-0" aria-hidden="true">
{{#str}} displayoptions, tiny_mediacms {{/str}}
</h3>
</div>
</legend>
<div id="vdisplayoptions" class="fcontainer collapseable collapse px-1">
<div class="tiny_mediacms_display_options">
<div class="mb-1">
<label for="vdisplayoptions_media-title-entry">{{#str}} entertitle, tiny_mediacms {{/str}}</label>
<input class="form-control fullwidth tiny_mediacms_title_entry" type="text" id="vdisplayoptions_media-title-entry"
size="32" value="{{video.title}}"/>
</div>
<div class="clearfix"></div>
<div class="mb-1">
<label>{{#str}} size, tiny_mediacms {{/str}}</label>
<div class="d-flex flex-wrap align-items-center tiny_mediacms_poster_size">
<label for="vdisplayoptions_media-width-entry" class="accesshide">{{#str}} videowidth, tiny_mediacms {{/str}}</label>
<input id="vdisplayoptions_media-width-entry" type="text" class="form-control w-auto me-1 tiny_mediacms_width_entry input-mini"
size="4" value="{{video.width}}"/>
x
<label for="vdisplayoptions_media-height-entry" class="accesshide">{{#str}} videoheight, tiny_mediacms {{/str}}</label>
<input id="vdisplayoptions_media-height-entry" type="text" class="form-control w-auto ms-1 tiny_mediacms_height_entry input-mini"
size="4" value="{{video.height}}"/>
</div>
</div>
<div class="clearfix"></div>
<div class="tiny_mediacms_source tiny_mediacms_poster_source">
<div class="mb-1">
<label for="display-video-url-input">
{{#str}} entersource, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="display-video-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32" value="{{video.poster}}"/>
{{#showfilepickerposter}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepickerposter}}
</div>
</div>
</div>
</div>
</div>
</fieldset>
<fieldset class="collapsible collapsed" id="{{elementid}}_video-advanced-settings">
<input name="mform_isexpanded_{{elementid}}_video-advanced-settings" type="hidden">
{{renderPartial "form_components.section" context=this id="vadvancedsettings" name="advancedsettings"}}
<legend class="d-flex align-items-center px-1">
<div class="position-relative d-flex ftoggler align-items-center position-relative me-1">
<a role="button" data-toggle="collapse" href="#vadvancedsettings" aria-expanded="false"
aria-controls="vadvancedsettings"
class="btn btn-icon me-3 icons-collapse-expand stretched-link fheader collapsed">
<span class="expanded-icon icon-no-margin p-2" title="{{#str}} collapse, moodle {{/str}}">
<i class="icon fa fa-chevron-down fa-fw " aria-hidden="true"></i>
</span>
<span class="collapsed-icon icon-no-margin p-2" title="{{#str}} expand, moodle {{/str}}">
<span class="dir-rtl-hide">
<i class="icon fa fa-chevron-right fa-fw " aria-hidden="true"></i>
</span>
<span class="dir-ltr-hide">
<i class="icon fa fa-chevron-left fa-fw " aria-hidden="true"></i>
</span>
</span>
<span class="sr-only">{{#str}} advancedsettings, tiny_mediacms {{/str}}</span>
</a>
<h3 class="d-flex align-self-stretch align-items-center mb-0" aria-hidden="true">
{{#str}} advancedsettings, tiny_mediacms {{/str}}
</h3>
</div>
</legend>
<div id="vadvancedsettings" class="fcontainer collapseable collapse px-1">
<div class="tiny_mediacms_advancedsettings">
<div class="form-check">
<input type="checkbox" checked="true" class="form-check-input tiny_mediacms_controls"
id="vadvancedsettings_media-controls-toggle" {{# video.controls }}checked{{/ video.controls }}/>
<label class="form-check-label" for="vadvancedsettings_media-controls-toggle">
{{#str}} controls, tiny_mediacms {{/str}}
</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_autoplay"
id="vadvancedsettings_media-autoplay-toggle" {{# video.autoplay }}checked{{/ video.autoplay }}/>
<label class="form-check-label" for="vadvancedsettings_media-autoplay-toggle">
{{#str}} autoplay, tiny_mediacms {{/str}}
</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_mute"
id="vadvancedsettings_media-mute-toggle" {{# video.muted }}checked{{/ video.muted }}/>
<label class="form-check-label" for="vadvancedsettings_media-mute-toggle">
{{#str}} mute, tiny_mediacms {{/str}}
</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_loop"
id="vadvancedsettings_media-loop-toggle" {{# video.loop }}checked{{/ video.loop }}/>
<label class="form-check-label" for="vadvancedsettings_media-loop-toggle">
{{#str}} loop, tiny_mediacms {{/str}}
</label>
</div>
</div>
</div>
</fieldset>
<fieldset class="collapsible collapsed" id="{{elementid}}_video-tracks">
<input name="mform_isexpanded_{{elementid}}_video-tracks" type="hidden">
<legend class="d-flex align-items-center px-1">
<div class="position-relative d-flex ftoggler align-items-center position-relative me-1">
<a role="button" data-toggle="collapse" href="#vtracks" aria-expanded="false"
aria-controls="vtracks"
class="btn btn-icon me-3 icons-collapse-expand stretched-link fheader collapsed">
<span class="expanded-icon icon-no-margin p-2" title="{{#str}} collapse, moodle {{/str}}">
<i class="icon fa fa-chevron-down fa-fw " aria-hidden="true"></i>
</span>
<span class="collapsed-icon icon-no-margin p-2" title="{{#str}} expand, moodle {{/str}}">
<span class="dir-rtl-hide">
<i class="icon fa fa-chevron-right fa-fw " aria-hidden="true"></i>
</span>
<span class="dir-ltr-hide">
<i class="icon fa fa-chevron-left fa-fw " aria-hidden="true"></i>
</span>
</span>
<span class="sr-only">{{#str}} tracks, tiny_mediacms {{/str}}</span>
</a>
<h3 class="d-flex align-self-stretch align-items-center mb-0" aria-hidden="true">
{{#str}} tracks, tiny_mediacms {{/str}}
</h3>
</div>
{{#trackshelpicon}}
{{> core/help_icon }}
{{/trackshelpicon}}
</legend>
<div id="vtracks" class="fcontainer collapseable collapse px-1">
<ul class="nav nav-tabs mb-3">
<li data-track-kind="subtitles" class="nav-item">
<a class="nav-link active" href="#{{elementid}}_vtracks_subtitles"
role="tab" data-toggle="tab">
{{#str}} subtitles, tiny_mediacms {{/str}}
</a>
</li>
<li data-track-kind="captions" class="nav-item">
<a class="nav-link" href="#{{elementid}}_vtracks_captions" role="tab" data-toggle="tab">
{{#str}} captions, tiny_mediacms {{/str}}
</a>
</li>
<li data-track-kind="descriptions" class="nav-item">
<a class="nav-link" href="#{{elementid}}_vtracks_descriptions"
role="tab" data-toggle="tab">
{{#str}} descriptions, tiny_mediacms {{/str}}
</a>
</li>
<li data-track-kind="chapters" class="nav-item">
<a class="nav-link" href="#{{elementid}}_vtracks_chapters" role="tab" data-toggle="tab">
{{#str}} chapters, tiny_mediacms {{/str}}
</a>
</li>
<li data-track-kind="metadata" class="nav-item">
<a class="nav-link" href="#{{elementid}}_vtracks_metadata" role="tab" data-toggle="tab">
{{#str}} metadata, tiny_mediacms {{/str}}
</a>
</li>
</ul>
<div class="tab-content">
<div data-track-kind="subtitles" class="tab-pane active"
id="{{elementid}}_vtracks_subtitles">
<div class="trackhelp">
{{#subtitleshelpicon}}
{{> core/help_icon }}
{{/subtitleshelpicon}}
</div>
{{#video.tracks.subtitles}}
<div class="mb-1 tiny_mediacms_track">
<div class="tiny_mediacms_source tiny_mediacms_track_source">
<div class="mb-1">
<label for="subtitle-video-url-input">
{{#str}} subtitlessourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="subtitle-video-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32" value="{{src}}"/>
{{#showfilepickertrack}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepickertrack}}
</div>
</div>
</div>
<div class="mb-3">
<label class="w-100" for="subtitle-video-lang-input">{{#str}} srclang, tiny_mediacms {{/str}}</label>
<select id="subtitle-video-lang-input" class="custom-select tiny_mediacms_track_lang_entry" data-value="{{srclang}}">
<optgroup label="{{#str}} languagesinstalled, tiny_mediacms {{/str}}">
{{#langsinstalled}}
<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>
{{/langsinstalled}}
</optgroup>
<optgroup label="{{#str}} languagesavailable, tiny_mediacms {{/str}} ">
{{#langsavailable}}
<option value="{{code}}">{{lang}}</option>
{{/langsavailable}}
</optgroup>
</select>
</div>
<div class="mb-3">
<label class="w-100" for="subtitle-video-track-input">{{#str}} label, tiny_mediacms {{/str}}</label>
<input id="subtitle-video-track-input" class="form-control tiny_mediacms_track_label_entry" type="text" value="{{label}}"/>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_track_default" {{# defaultTrack }}checked{{/ defaultTrack }}/>
<label class="form-check-label">{{#str}} default, tiny_mediacms {{/str}}</label>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} addsubtitlestrack, tiny_mediacms {{/str}}
</a>
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/video.tracks.subtitles}}
{{^video.tracks.subtitles}}
<div class="mb-1 tiny_mediacms_track">
<div class="tiny_mediacms_source tiny_mediacms_track_source">
<div class="mb-1">
<label for="subtitle-video-url-input">
{{#str}} subtitlessourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="subtitle-video-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32"/>
{{#showfilepickertrack}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepickertrack}}
</div>
</div>
</div>
<div class="mb-3">
<label class="w-100" for="subtitle-video-lang-input">{{#str}} srclang, tiny_mediacms {{/str}}</label>
<select id="subtitle-video-lang-input" class="custom-select tiny_mediacms_track_lang_entry">
<optgroup label="{{#str}} languagesinstalled, tiny_mediacms {{/str}}">
{{#langsinstalled}}
<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>
{{/langsinstalled}}
</optgroup>
<optgroup label="{{#str}} languagesavailable, tiny_mediacms {{/str}} ">
{{#langsavailable}}
<option value="{{code}}">{{lang}}</option>
{{/langsavailable}}
</optgroup>
</select>
</div>
<div class="mb-3">
<label class="w-100" for="subtitle-video-track-input">{{#str}} label, tiny_mediacms {{/str}}</label>
<input id="subtitle-video-track-input" class="form-control tiny_mediacms_track_label_entry" type="text"/>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_track_default"/>
<label class="form-check-label">{{#str}} default, tiny_mediacms {{/str}}</label>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} addsubtitlestrack, tiny_mediacms {{/str}}
</a>
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/video.tracks.subtitles}}
</div>
<div data-track-kind="captions" class="tab-pane"
id="{{elementid}}_vtracks_captions">
<div class="trackhelp">
{{#captionshelpicon}}
{{> core/help_icon }}
{{/captionshelpicon}}
</div>
{{#video.tracks.captions}}
<div class="mb-1 tiny_mediacms_track">
<div class="tiny_mediacms_source tiny_mediacms_track_source">
<div class="mb-1">
<label for="caption-video-url-input">
{{#str}} captionssourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="caption-video-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32" value="{{src}}"/>
{{#showfilepickertrack}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepickertrack}}
</div>
</div>
</div>
<div class="mb-3">
<label class="w-100" for="caption-video-lang-input">{{#str}} srclang, tiny_mediacms {{/str}}</label>
<select id="caption-video-lang-input" class="custom-select tiny_mediacms_track_lang_entry" data-value="{{srclang}}">
<optgroup label="{{#str}} languagesinstalled, tiny_mediacms {{/str}}">
{{#langsinstalled}}
<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>
{{/langsinstalled}}
</optgroup>
<optgroup label="{{#str}} languagesavailable, tiny_mediacms {{/str}} ">
{{#langsavailable}}
<option value="{{code}}">{{lang}}</option>
{{/langsavailable}}
</optgroup>
</select>
</div>
<div class="mb-3">
<label class="w-100" for="caption-video-track-input">{{#str}} label, tiny_mediacms {{/str}}</label>
<input id="caption-video-track-input" class="form-control tiny_mediacms_track_label_entry" type="text" value="{{label}}"/>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_track_default" {{# defaultTrack }}checked{{/ defaultTrack }}/>
<label class="form-check-label">{{#str}} default, tiny_mediacms {{/str}}</label>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} addcaptionstrack, tiny_mediacms {{/str}}
</a>
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/video.tracks.captions}}
{{^video.tracks.captions}}
<div class="mb-1 tiny_mediacms_track">
<div class="tiny_mediacms_source tiny_mediacms_track_source">
<div class="mb-1">
<label for="caption-video-url-input">
{{#str}} captionssourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="caption-video-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32"/>
{{#showfilepickertrack}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepickertrack}}
</div>
</div>
</div>
<div class="mb-3">
<label class="w-100" for="caption-video-lang-input">{{#str}} srclang, tiny_mediacms {{/str}}</label>
<select id="caption-video-lang-input" class="custom-select tiny_mediacms_track_lang_entry">
<optgroup label="{{#str}} languagesinstalled, tiny_mediacms {{/str}}">
{{#langsinstalled}}
<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>
{{/langsinstalled}}
</optgroup>
<optgroup label="{{#str}} languagesavailable, tiny_mediacms {{/str}} ">
{{#langsavailable}}
<option value="{{code}}">{{lang}}</option>
{{/langsavailable}}
</optgroup>
</select>
</div>
<div class="mb-3">
<label class="w-100" for="caption-video-track-input">{{#str}} label, tiny_mediacms {{/str}}</label>
<input id="caption-video-track-input" class="form-control tiny_mediacms_track_label_entry" type="text"/>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_track_default"/>
<label class="form-check-label">{{#str}} default, tiny_mediacms {{/str}}</label>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} addcaptionstrack, tiny_mediacms {{/str}}
</a>
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/video.tracks.captions}}
</div>
<div data-track-kind="descriptions" class="tab-pane"
id="{{elementid}}_vtracks_descriptions">
<div class="trackhelp">
{{#descriptionshelpicon}}
{{> core/help_icon }}
{{/descriptionshelpicon}}
</div>
{{#video.tracks.descriptions}}
<div class="mb-1 tiny_mediacms_track">
<div class="tiny_mediacms_source tiny_mediacms_track_source">
<div class="mb-1">
<label for="description-video-url-input">
{{#str}} descriptionssourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="description-video-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32" value="{{src}}"/>
{{#showfilepickertrack}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepickertrack}}
</div>
</div>
</div>
<div class="mb-3">
<label class="w-100" for="description-video-lang-input">{{#str}} srclang, tiny_mediacms {{/str}}</label>
<select id="description-video-ang-input" class="custom-select tiny_mediacms_track_lang_entry" data-value="{{srclang}}">
<optgroup label="{{#str}} languagesinstalled, tiny_mediacms {{/str}}">
{{#langsinstalled}}
<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>
{{/langsinstalled}}
</optgroup>
<optgroup label="{{#str}} languagesavailable, tiny_mediacms {{/str}} ">
{{#langsavailable}}
<option value="{{code}}">{{lang}}</option>
{{/langsavailable}}
</optgroup>
</select>
</div>
<div class="mb-3">
<label class="w-100" for="description-video-track-input">{{#str}} label, tiny_mediacms {{/str}}</label>
<input id="description-video-track-input" class="form-control tiny_mediacms_track_label_entry" type="text" value="{{label}}"/>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_track_default" {{# defaultTrack }}checked{{/ defaultTrack }}/>
<label class="form-check-label">{{#str}} default, tiny_mediacms {{/str}}</label>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} adddescriptionstrack, tiny_mediacms {{/str}}
</a>
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/video.tracks.descriptions}}
{{^video.tracks.descriptions}}
<div class="mb-1 tiny_mediacms_track">
<div class="tiny_mediacms_source tiny_mediacms_track_source">
<div class="mb-1">
<label for="description-video-url-input">
{{#str}} descriptionssourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="description-video-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32"/>
{{#showfilepickertrack}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepickertrack}}
</div>
</div>
</div>
<div class="mb-3">
<label class="w-100" for="description-video-lang-input">{{#str}} srclang, tiny_mediacms {{/str}}</label>
<select id="description-video-lang-input" class="custom-select tiny_mediacms_track_lang_entry">
<optgroup label="{{#str}} languagesinstalled, tiny_mediacms {{/str}}">
{{#langsinstalled}}
<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>
{{/langsinstalled}}
</optgroup>
<optgroup label="{{#str}} languagesavailable, tiny_mediacms {{/str}} ">
{{#langsavailable}}
<option value="{{code}}">{{lang}}</option>
{{/langsavailable}}
</optgroup>
</select>
</div>
<div class="mb-3">
<label class="w-100" for="description-video-track-input">{{#str}} label, tiny_mediacms {{/str}}</label>
<input id="description-video-track-input" class="form-control tiny_mediacms_track_label_entry" type="text"/>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_track_default"/>
<label class="form-check-label">{{#str}} default, tiny_mediacms {{/str}}</label>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} adddescriptionstrack, tiny_mediacms {{/str}}
</a>
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/video.tracks.descriptions}}
</div>
<div data-track-kind="chapters" class="tab-pane"
id="{{elementid}}_vtracks_chapters">
<div class="trackhelp">
{{#chaptershelpicon}}
{{> core/help_icon }}
{{/chaptershelpicon}}
</div>
{{#video.tracks.chapters}}
<div class="mb-1 tiny_mediacms_track">
<div class="tiny_mediacms_source tiny_mediacms_track_source">
<div class="mb-1">
<label for="chapter-video-url-input">
{{#str}} chapterssourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="chapter-video-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32" value="{{src}}"/>
{{#showfilepickertrack}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepickertrack}}
</div>
</div>
</div>
<div class="mb-3">
<label class="w-100" for="chapter-video-lang-input">{{#str}} srclang, tiny_mediacms {{/str}}</label>
<select id="chapter-video-lang-input" class="custom-select tiny_mediacms_track_lang_entry" data-value="{{srclang}}">
<optgroup label="{{#str}} languagesinstalled, tiny_mediacms {{/str}}">
{{#langsinstalled}}
<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>
{{/langsinstalled}}
</optgroup>
<optgroup label="{{#str}} languagesavailable, tiny_mediacms {{/str}} ">
{{#langsavailable}}
<option value="{{code}}">{{lang}}</option>
{{/langsavailable}}
</optgroup>
</select>
</div>
<div class="mb-3">
<label class="w-100" for="chapter-video-track-input">{{#str}} label, tiny_mediacms {{/str}}</label>
<input id="chapter-video-track-input" class="form-control tiny_mediacms_track_label_entry" type="text" value="{{label}}"/>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_track_default" {{# defaultTrack }}checked{{/ defaultTrack }}/>
<label class="form-check-label">{{#str}} default, tiny_mediacms {{/str}}</label>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} addchapterstrack, tiny_mediacms {{/str}}
</a>
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/video.tracks.chapters}}
{{^video.tracks.chapters}}
<div class="mb-1 tiny_mediacms_track">
<div class="tiny_mediacms_source tiny_mediacms_track_source">
<div class="mb-1">
<label for="chapter-video-url-input">
{{#str}} chapterssourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="chapter-video-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32"/>
{{#showfilepickertrack}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepickertrack}}
</div>
</div>
</div>
<div class="mb-3">
<label class="w-100" for="chapter-video-lang-input">{{#str}} srclang, tiny_mediacms {{/str}}</label>
<select id="chapter-video-lang-input" class="custom-select tiny_mediacms_track_lang_entry">
<optgroup label="{{#str}} languagesinstalled, tiny_mediacms {{/str}}">
{{#langsinstalled}}
<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>
{{/langsinstalled}}
</optgroup>
<optgroup label="{{#str}} languagesavailable, tiny_mediacms {{/str}} ">
{{#langsavailable}}
<option value="{{code}}">{{lang}}</option>
{{/langsavailable}}
</optgroup>
</select>
</div>
<div class="mb-3">
<label class="w-100" for="chapter-video-track-input">{{#str}} label, tiny_mediacms {{/str}}</label>
<input id="chapter-video-track-input" class="form-control tiny_mediacms_track_label_entry" type="text"/>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_track_default"/>
<label class="form-check-label">{{#str}} default, tiny_mediacms {{/str}}</label>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} addchapterstrack, tiny_mediacms {{/str}}
</a>
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/video.tracks.chapters}}
</div>
<div data-track-kind="metadata" class="tab-pane"
id="{{elementid}}_vtracks_metadata">
<div class="trackhelp">{{{helpStrings.metadata}}}</div>
<div class="trackhelp">
{{#metadatahelpicon}}
{{> core/help_icon }}
{{/metadatahelpicon}}
</div>
{{#video.tracks.metadata}}
<div class="mb-1 tiny_mediacms_track">
<div class="tiny_mediacms_source tiny_mediacms_track_source">
<div class="mb-1">
<label for="metadata-video-url-input">
{{#str}} metadatasourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="metadata-video-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32" value="{{src}}"/>
{{#showfilepickertrack}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepickertrack}}
</div>
</div>
</div>
<div class="mb-3">
<label class="w-100" for="metadata-video-lang-input">{{#str}} srclang, tiny_mediacms {{/str}}</label>
<select id="metadata-video-lang-input" class="custom-select tiny_mediacms_track_lang_entry" data-value="{{srclang}}">
<optgroup label="{{#str}} languagesinstalled, tiny_mediacms {{/str}}">
{{#langsinstalled}}
<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>
{{/langsinstalled}}
</optgroup>
<optgroup label="{{#str}} languagesavailable, tiny_mediacms {{/str}} ">
{{#langsavailable}}
<option value="{{code}}">{{lang}}</option>
{{/langsavailable}}
</optgroup>
</select>
</div>
<div class="mb-3">
<label class="w-100" for="metadata-video-track-input">{{#str}} label, tiny_mediacms {{/str}}</label>
<input id="metadata-video-track-input" class="form-control tiny_mediacms_track_label_entry" type="text" value="{{label}}"/>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_track_default" {{# defaultTrack }}checked{{/ defaultTrack }}/>
<label class="form-check-label">{{#str}} default, tiny_mediacms {{/str}}</label>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} addmetadatatrack, tiny_mediacms {{/str}}
</a>
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/video.tracks.metadata}}
{{^video.tracks.metadata}}
<div class="mb-1 tiny_mediacms_track">
<div class="tiny_mediacms_source tiny_mediacms_track_source">
<div class="mb-1">
<label for="metadata-video-url-input">
{{#str}} metadatasourcelabel, tiny_mediacms {{/str}}
</label>
<div class="input-group input-append w-100">
<input id="metadata-video-url-input" class="form-control tiny_mediacms_url_entry" type="url" size="32"/>
{{#showfilepickertrack}}
<span class="input-group-append">
<button class="btn btn-secondary openmediacmsbrowser" type="button">{{#str}} browserepositories, tiny_mediacms {{/str}}</button>
</span>
{{/showfilepickertrack}}
</div>
</div>
</div>
<div class="mb-3">
<label class="w-100" for="metadata-video-lang-input">{{#str}} srclang, tiny_mediacms {{/str}}</label>
<select id="metadata-video-lang-input" class="custom-select tiny_mediacms_track_lang_entry">
<optgroup label="{{#str}} languagesinstalled, tiny_mediacms {{/str}}">
{{#langsinstalled}}
<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>
{{/langsinstalled}}
</optgroup>
<optgroup label="{{#str}} languagesavailable, tiny_mediacms {{/str}} ">
{{#langsavailable}}
<option value="{{code}}">{{lang}}</option>
{{/langsavailable}}
</optgroup>
</select>
</div>
<div class="mb-3">
<label class="w-100" for="metadata-video-track-input">{{#str}} label, tiny_mediacms {{/str}}</label>
<input id="metadata-video-track-input" class="form-control tiny_mediacms_track_label_entry" type="text"/>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_mediacms_track_default"/>
<label class="form-check-label">{{#str}} default, tiny_mediacms {{/str}}</label>
</div>
<div class="addcomponent-wrapper">
<a href="#" class="addcomponent">
{{#str}} addmetadatatrack, tiny_mediacms {{/str}}
</a>
</div>
<div class="removecomponent-wrapper hidden">
<a href="#" class="removecomponent">
{{#str}} remove, tiny_mediacms {{/str}}
</a>
</div>
</div>
{{/video.tracks.metadata}}
</div>
</div>
</div>
</fieldset>

View File

@@ -0,0 +1,50 @@
{{!
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/>.
}}
{{!
@template tiny_mediacms/embed_media_video
Embed media video template.
Example context (json):
{
}
}}
&nbsp;<video{{!
}}{{#width}} width="{{.}}"{{/width}}{{!
}}{{#height}} height="{{.}}"{{/height}}{{!
}}{{#poster}} poster="{{.}}"{{/poster}}{{!
}}{{#showControls}} controls="true"{{/showControls}}{{!
}}{{#loop}} loop="true"{{/loop}}{{!
}}{{#muted}} muted="true"{{/muted}}{{!
}}{{#autoplay}} autoplay="true"{{/autoplay}}{{!
}}{{#title}} title="{{.}}"{{/title}}{{!
}}>
{{#sources}}
<source src="{{.}}">
{{/sources}}
{{#tracks}}
<track{{!
}} src="{{track}}"{{!
}} kind="{{kind}}"{{!
}} srclang="{{srclang}}"{{!
}} label="{{label}}"{{!
}}{{#defaultTrack}} default="true"{{/defaultTrack}}{{!
}}>
{{/tracks}}
{{#description}}{{.}}{{/description}}
</video>&nbsp;

View File

@@ -0,0 +1,134 @@
{{!
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/>.
}}
{{!
@template tiny_mediacms/iframe_embed_modal
Iframe embed modal template.
Example context (json):
{
"elementid": "editor1",
"isupdating": false
}
}}
{{< core/modal }}
{{$body}}
<form class="tiny_iframecms_form" id="{{elementid}}_tiny_iframecms_form">
<!-- Tab Navigation -->
<ul class="nav nav-tabs mb-3 tiny_iframecms_tabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active tiny_iframecms_tab_url_btn" id="{{elementid}}_tab_url"
data-bs-toggle="tab" data-bs-target="#{{elementid}}_pane_url"
type="button" role="tab" aria-controls="{{elementid}}_pane_url" aria-selected="true">
{{#str}} tabembedurl, tiny_mediacms {{/str}}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link tiny_iframecms_tab_iframe_library_btn" id="{{elementid}}_tab_iframe_library"
data-bs-toggle="tab" data-bs-target="#{{elementid}}_pane_iframe_library"
type="button" role="tab" aria-controls="{{elementid}}_pane_iframe_library" aria-selected="false">
{{#str}} tabvideolibraryiframe, tiny_mediacms {{/str}}
</button>
</li>
</ul>
<!-- Tab Content -->
<div class="tab-content">
<!-- Tab 1: Embed URL (existing content) -->
<div class="tab-pane fade show active tiny_iframecms_pane_url" id="{{elementid}}_pane_url" role="tabpanel" aria-labelledby="{{elementid}}_tab_url">
<div class="container-fluid p-0">
<div class="row">
<!-- Left column: URL and Options -->
<div class="col-md-6">
<!-- URL Input -->
<div class="mb-3">
<label for="{{elementid}}_iframe_url" class="form-label font-weight-bold">
{{#str}} iframeurl, tiny_mediacms {{/str}}
</label>
<textarea
class="form-control tiny_iframecms_url"
id="{{elementid}}_iframe_url"
rows="3"
placeholder="{{#str}} iframeurlplaceholder, tiny_mediacms {{/str}}"
>{{url}}</textarea>
<div class="tiny_iframecms_url_warning text-danger small mt-1 d-none">
{{#str}} iframeurlinvalid, tiny_mediacms {{/str}}
</div>
</div>
{{> tiny_mediacms/iframe_embed_options }}
</div>
<!-- Right column: Preview -->
<div class="col-md-6">
<label class="form-label font-weight-bold">
{{#str}} preview, tiny_mediacms {{/str}}
</label>
<div class="tiny_iframecms_preview_container border rounded p-2 bg-light" style="min-height: 300px;">
<div class="tiny_iframecms_preview d-flex align-items-center justify-content-center text-muted" style="min-height: 280px;">
<span>{{#str}} iframeurlplaceholder, tiny_mediacms {{/str}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tab 2: Media Library -->
<div class="tab-pane fade tiny_iframecms_pane_iframe_library" id="{{elementid}}_pane_iframe_library" role="tabpanel" aria-labelledby="{{elementid}}_tab_iframe_library">
<div class="tiny_iframecms_iframe_library_container" style="min-height: 500px;">
<div class="tiny_iframecms_iframe_library_placeholder text-center py-5">
<p class="text-muted">{{#str}} libraryloading, tiny_mediacms {{/str}}</p>
</div>
<div class="tiny_iframecms_iframe_library_loading text-center py-5 d-none">
<div class="spinner-border text-primary" role="status">
<span class="sr-only visually-hidden">{{#str}} libraryloading, tiny_mediacms {{/str}}</span>
</div>
<p class="mt-2 text-muted">{{#str}} libraryloading, tiny_mediacms {{/str}}</p>
</div>
<iframe
class="tiny_iframecms_iframe_library_frame d-none"
src=""
style="width: 100%; height: 500px; border: 1px solid #dee2e6; border-radius: 0.25rem;"
frameborder="0"
allowfullscreen>
</iframe>
</div>
</div>
</div>
</form>
{{/body}}
{{$footer}}
<button type="button" class="btn btn-primary" data-action="save">
{{#isupdating}}
{{#str}} updateiframe, tiny_mediacms {{/str}}
{{/isupdating}}
{{^isupdating}}
{{#str}} insertiframe, tiny_mediacms {{/str}}
{{/isupdating}}
</button>
{{#isupdating}}
<button type="button" class="btn btn-danger tiny_iframecms_remove_btn" data-action="remove">
{{#str}} removeiframe, tiny_mediacms {{/str}}
</button>
{{/isupdating}}
<button type="button" class="btn btn-secondary" data-action="cancel">{{#str}} cancel, moodle {{/str}}</button>
{{/footer}}
{{/ core/modal }}

View File

@@ -0,0 +1,127 @@
{{!
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/>.
}}
{{!
@template tiny_mediacms/iframe_embed_options
Embed options partial for iframe modal.
Example context (json):
{
"elementid": "editor1",
"showTitle": true,
"linkTitle": true,
"showRelated": true,
"showUserAvatar": true,
"responsive": true,
"startAtEnabled": false,
"startAt": "0:00"
}
}}
<!-- Embed Options -->
<div class="mb-3">
<label class="form-label font-weight-bold">
{{#str}} embedoptions, tiny_mediacms {{/str}}
</label>
<div class="row">
<div class="col-6">
<div class="form-check mb-2">
<input type="checkbox" class="form-check-input tiny_iframecms_showtitle"
id="{{elementid}}_showtitle" {{#showTitle}}checked{{/showTitle}}>
<label class="form-check-label" for="{{elementid}}_showtitle">
{{#str}} showtitle, tiny_mediacms {{/str}}
</label>
</div>
<div class="form-check mb-2">
<input type="checkbox" class="form-check-input tiny_iframecms_linktitle"
id="{{elementid}}_linktitle" {{#linkTitle}}checked{{/linkTitle}}>
<label class="form-check-label" for="{{elementid}}_linktitle">
{{#str}} linktitle, tiny_mediacms {{/str}}
</label>
</div>
<div class="form-check mb-2">
<input type="checkbox" class="form-check-input tiny_iframecms_showrelated"
id="{{elementid}}_showrelated" {{#showRelated}}checked{{/showRelated}}>
<label class="form-check-label" for="{{elementid}}_showrelated">
{{#str}} showrelated, tiny_mediacms {{/str}}
</label>
</div>
</div>
<div class="col-6">
<div class="form-check mb-2">
<input type="checkbox" class="form-check-input tiny_iframecms_showuseravatar"
id="{{elementid}}_showuseravatar" {{#showUserAvatar}}checked{{/showUserAvatar}}>
<label class="form-check-label" for="{{elementid}}_showuseravatar">
{{#str}} showuseravatar, tiny_mediacms {{/str}}
</label>
</div>
<div class="form-check mb-2">
<input type="checkbox" class="form-check-input tiny_iframecms_responsive"
id="{{elementid}}_responsive" {{#responsive}}checked{{/responsive}}>
<label class="form-check-label" for="{{elementid}}_responsive">
{{#str}} responsive, tiny_mediacms {{/str}}
</label>
</div>
<div class="form-check mb-2 d-flex align-items-center">
<input type="checkbox" class="form-check-input tiny_iframecms_startat_enabled"
id="{{elementid}}_startat_enabled" {{#startAtEnabled}}checked{{/startAtEnabled}}>
<label class="form-check-label ms-2 me-2" for="{{elementid}}_startat_enabled">
{{#str}} startat, tiny_mediacms {{/str}}
</label>
<input type="text" class="form-control form-control-sm tiny_iframecms_startat"
id="{{elementid}}_startat" value="{{startAt}}" placeholder="0:00" style="width: 70px;">
</div>
</div>
</div>
</div>
<!-- Aspect Ratio -->
<div class="mb-3">
<label for="{{elementid}}_aspectratio" class="form-label font-weight-bold">
{{#str}} aspectratio, tiny_mediacms {{/str}}
</label>
<select class="form-control tiny_iframecms_aspectratio" id="{{elementid}}_aspectratio">
<option value="16:9" {{#is16_9}}selected{{/is16_9}}>{{#str}} aspectratio_16_9, tiny_mediacms {{/str}}</option>
<option value="4:3" {{#is4_3}}selected{{/is4_3}}>{{#str}} aspectratio_4_3, tiny_mediacms {{/str}}</option>
<option value="1:1" {{#is1_1}}selected{{/is1_1}}>{{#str}} aspectratio_1_1, tiny_mediacms {{/str}}</option>
<option value="custom" {{#isCustom}}selected{{/isCustom}}>{{#str}} aspectratio_custom, tiny_mediacms {{/str}}</option>
</select>
</div>
<!-- Dimensions -->
<div class="mb-3">
<label class="form-label font-weight-bold">
{{#str}} dimensions, tiny_mediacms {{/str}}
</label>
<div class="row">
<div class="col-6">
<div class="input-group">
<input type="number" class="form-control tiny_iframecms_width"
id="{{elementid}}_width" value="{{width}}" placeholder="560">
<span class="input-group-text">px</span>
</div>
<small class="text-muted">{{#str}} width, tiny_mediacms {{/str}}</small>
</div>
<div class="col-6">
<div class="input-group">
<input type="number" class="form-control tiny_iframecms_height"
id="{{elementid}}_height" value="{{height}}" placeholder="315">
<span class="input-group-text">px</span>
</div>
<small class="text-muted">{{#str}} height, tiny_mediacms {{/str}}</small>
</div>
</div>
</div>

View File

@@ -0,0 +1,36 @@
{{!
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/>.
}}
{{!
@template tiny_mediacms/iframe_embed_output
Iframe embed output template.
Example context (json):
{
"src": "https://example.com/embed?m=abc123",
"width": 560,
"height": 315,
"responsive": true,
"aspectRatioClass": "ratio-16-9"
}
}}
{{#responsive}}
<iframe src="{{src}}" style="width:100%;aspect-ratio:{{aspectRatioValue}};display:block;border:0;" allowFullScreen></iframe>
{{/responsive}}
{{^responsive}}
<iframe width="{{width}}" height="{{height}}" src="{{src}}" frameBorder="0" allowFullScreen></iframe>
{{/responsive}}

View File

@@ -0,0 +1,91 @@
{{!
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/>.
}}
{{!
@template tiny_mediacms/iframe_video_library
Video library browser for iframe modal.
Example context (json):
{
"elementid": "editor1"
}
}}
<div class="tiny_iframecms_library_container">
<!-- Search and Filter Bar -->
<div class="row mb-3">
<div class="col-md-8">
<div class="input-group">
<input type="text" class="form-control tiny_iframecms_library_search"
placeholder="{{#str}} librarysearchplaceholder, tiny_mediacms {{/str}}">
<button type="button" class="btn btn-outline-secondary tiny_iframecms_library_search_btn">
<i class="fa fa-search" aria-hidden="true"></i>
{{#str}} search, moodle {{/str}}
</button>
</div>
</div>
<div class="col-md-4">
<select class="form-control tiny_iframecms_library_sort">
<option value="date_desc">{{#str}} librarysortnewest, tiny_mediacms {{/str}}</option>
<option value="date_asc">{{#str}} librarysortoldest, tiny_mediacms {{/str}}</option>
<option value="title_asc">{{#str}} librarysorttitle, tiny_mediacms {{/str}}</option>
<option value="views_desc">{{#str}} librarysortviews, tiny_mediacms {{/str}}</option>
</select>
</div>
</div>
<!-- Video Grid -->
<div class="tiny_iframecms_library_grid" style="max-height: 400px; overflow-y: auto;">
<!-- Loading state -->
<div class="tiny_iframecms_library_loading text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">{{#str}} loading, tiny_mediacms {{/str}}</span>
</div>
<p class="mt-2 text-muted">{{#str}} libraryloading, tiny_mediacms {{/str}}</p>
</div>
<!-- Videos will be rendered here -->
<div class="tiny_iframecms_library_items row d-none">
<!-- Video items will be dynamically inserted here -->
</div>
<!-- Empty state -->
<div class="tiny_iframecms_library_empty text-center py-5 d-none">
<i class="fa fa-video-camera fa-3x text-muted mb-3" aria-hidden="true"></i>
<p class="text-muted">{{#str}} libraryempty, tiny_mediacms {{/str}}</p>
</div>
<!-- Error state -->
<div class="tiny_iframecms_library_error text-center py-5 d-none">
<i class="fa fa-exclamation-triangle fa-3x text-warning mb-3" aria-hidden="true"></i>
<p class="text-danger tiny_iframecms_library_error_message">{{#str}} libraryerror, tiny_mediacms {{/str}}</p>
<button type="button" class="btn btn-secondary tiny_iframecms_library_retry">
{{#str}} libraryretry, tiny_mediacms {{/str}}
</button>
</div>
</div>
<!-- Pagination -->
<div class="tiny_iframecms_library_pagination d-flex justify-content-between align-items-center mt-3 d-none">
<button type="button" class="btn btn-outline-secondary btn-sm tiny_iframecms_library_prev" disabled>
<i class="fa fa-chevron-left" aria-hidden="true"></i> {{#str}} libraryprev, tiny_mediacms {{/str}}
</button>
<span class="tiny_iframecms_library_page_info text-muted"></span>
<button type="button" class="btn btn-outline-secondary btn-sm tiny_iframecms_library_next">
{{#str}} librarynext, tiny_mediacms {{/str}} <i class="fa fa-chevron-right" aria-hidden="true"></i>
</button>
</div>
</div>

View File

@@ -0,0 +1,73 @@
{{!
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/>.
}}
{{!
@template tiny_mediacms/iframe_video_library_item
Single video item in the library grid.
Example context (json):
{
"id": "abc123",
"title": "My Video",
"thumbnail": "https://example.com/thumb.jpg",
"duration": "2:30",
"views": "150 views",
"date": "2 weeks ago",
"embedUrl": "https://example.com/embed?m=abc123"
}
}}
<div class="col-md-4 col-sm-6 mb-3">
<div class="tiny_iframecms_library_item card h-100"
data-video-id="{{id}}"
data-embed-url="{{embedUrl}}"
style="cursor: pointer;">
<div class="position-relative">
<img src="{{thumbnail}}" class="card-img-top" alt="{{title}}"
style="height: 120px; object-fit: cover;"
onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22320%22 height=%22120%22><rect fill=%22%23ddd%22 width=%22320%22 height=%22120%22/><text x=%2250%%22 y=%2250%%22 dominant-baseline=%22middle%22 text-anchor=%22middle%22 fill=%22%23999%22 font-size=%2214%22>No thumbnail</text></svg>'">
{{#duration}}
<span class="position-absolute bottom-0 end-0 bg-dark text-white px-1 m-1 small rounded"
style="font-size: 0.75rem;">
{{duration}}
</span>
{{/duration}}
<!-- Select overlay button -->
<div class="tiny_iframecms_library_item_overlay position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center"
style="background: rgba(0,0,0,0.5); opacity: 0; transition: opacity 0.2s;">
<button type="button" class="btn btn-primary btn-sm tiny_iframecms_library_select_btn">
{{#str}} libraryselect, tiny_mediacms {{/str}}
</button>
</div>
</div>
<div class="card-body p-2">
<h6 class="card-title mb-1 text-truncate" title="{{title}}" style="font-size: 0.875rem;">
{{title}}
</h6>
<p class="card-text text-muted mb-0" style="font-size: 0.75rem;">
{{#views}}{{views}} &bull; {{/views}}{{date}}
</p>
</div>
</div>
</div>
<style>
.tiny_iframecms_library_item:hover .tiny_iframecms_library_item_overlay {
opacity: 1 !important;
}
.tiny_iframecms_library_item:hover {
box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.15);
}
</style>

View File

@@ -0,0 +1,34 @@
{{!
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/>.
}}
{{!
@template tiny_mediacms/image
Image template.
Example context (json):
{
}
}}
<img src="{{url}}" alt="{{alt}}"{{!
}}{{#width}} width="{{.}}"{{/width}}{{!
}}{{#height}} height="{{.}}"{{/height}}{{!
}}{{#presentation}} role="presentation"{{/presentation}}{{!
}}{{#customStyle}} style="{{.}}"{{/customStyle}}{{!
}}{{#classlist}} class="{{.}}"{{/classlist}}{{!
}}{{#id}} id="{{.}}"{{/id}}{{!
}}/>

View File

@@ -0,0 +1,61 @@
{{!
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/>.
}}
{{!
@template tiny_mediacms/insert_image_modal
Insert image template.
Example context (json):
{
"uniqid": 0,
"elementid": "exampleId",
"loading": {},
"title": "Insert image"
}
}}
{{< core/modal }}
{{$body}}
<form class="tiny_imagecms_form">
<!-- URL warning -->
<div role="alert" class="d-none alert alert-warning mb-1 tiny_imagecms_urlwarning">
<label>
{{#str}} imageurlrequired, tiny_mediacms {{/str}}
</label>
</div>
<!-- Presentation warning -->
<div role="alert" class="d-none tiny_imagecms_altwarning alert alert-warning mb-1">
<label>
{{#str}} presentationoraltrequired, tiny_mediacms {{/str}}
</label>
</div>
<!-- Preloader icon -->
<div role="progressbar" class="d-none tiny_imagecms_loader lead border rounded text-center">
<div class="tiny_imagecms_loader_container d-flex flex-column justify-content-center">
<span data-region="loading-icon-container">
{{> core/loading }}
</span>
<div>{{#str}} loading, tiny_mediacms {{/str}}</div>
</div>
</div>
<div class="tiny_imagecms_body_template"></div>
</form>
{{/body}}
{{$footer}}
<div class="tiny_imagecms_footer_template container"></div>
{{/footer}}
{{/ core/modal }}

View File

@@ -0,0 +1,112 @@
{{!
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/>.
}}
{{!
@template tiny_mediacms/insert_image_modal_details
Insert image details body template.
Example context (json):
{
"elementid": "exampleId",
"alt": "Image description",
"presentation": true,
"width": 600,
"height": 400,
"customStyle": "",
"sizecustomhelpicon": {
"text": "Help text"
}
}
}}
<div class="tiny_imagecms_image_details">
<div class="container">
<div class="row">
<!-- Column 1: Image Preview and Description -->
<div class="tiny_imagecms_preview_col col-lg-7 p-0">
<input type="hidden" class="tiny_imagecms_customstyle" value="{{customStyle}}">
<!-- Row 1: Image preview -->
<div class="tiny_imagecms_preview_box border rounded">
<!-- Delete image icon -->
<div class="tiny_imagecms_deleteicon" tabindex="0" title="{{#str}} deleteimage, tiny_mediacms {{/str}}">
<i class="fa fa-trash-o" title="{{#str}} deleteimage, tiny_mediacms {{/str}}"></i>
</div>
<!-- Image placeholder -->
<img class="tiny_imagecms_preview" src="data:," alt>
</div>
<!-- Row 2: Image description -->
<div class="form-group mt-3">
<label for="{{elementid}}_tiny_imagecms_altentry">{{#str}} enteralt, tiny_mediacms {{/str}}</label>
<textarea class="tiny_imagecms_altentry form-control fullwidth" id="{{elementid}}_tiny_imagecms_altentry" name="altentry" maxlength="125">{{alt}}</textarea>
<!-- Character counter -->
<div id="the-count" class="d-flex justify-content-end small">
<span id="currentcount">0</span>
<span id="maximumcount"> / 125</span>
</div>
</div>
</div>
<!-- Column 2: Checkbox and Radio Buttons -->
<div class="tiny_imagecms_properties_col col-lg-5">
<!-- Row 1: Image presentation role -->
<div class="form-check mb-2">
<input type="checkbox" class="tiny_imagecms_presentation form-check-input" id="{{elementid}}_tiny_imagecms_presentation" {{# presentation }}checked{{/ presentation }}>
<label class="form-check-label" for="{{elementid}}_tiny_imagecms_presentation">{{#str}} presentation, tiny_mediacms {{/str}}</label>
</div>
<!-- Row 2: Original size radiobutton -->
<div class="form-check mb-2 ps-0">
<input type="radio" class="tiny_imagecms_sizeoriginal" id="{{elementid}}_tiny_imagecms_sizeoriginal" name="radioOptions">
<label class="form-check-label" for="{{elementid}}_tiny_imagecms_sizeoriginal">{{#str}} sizeoriginal, tiny_mediacms {{/str}}</label>
</div>
<!-- Row 3: Custom size radiobutton -->
<div class="form-check ps-0 mb-2">
<input type="radio" class="tiny_imagecms_sizecustom" id="{{elementid}}_tiny_imagecms_sizecustom" name="radioOptions">
<label class="form-check-label" for="{{elementid}}_tiny_imagecms_sizecustom">{{#str}} sizecustom, tiny_mediacms {{/str}}</label>
</div>
<!-- Row 4: Image size -->
<div class="tiny_imagecms_properties mb-2">
<!-- Row 1: Image width and height -->
<div id="{{elementid}}_tiny_imagecms_size" class="tiny_imagecms_size container ms-1">
<div class="d-flex justify-content-start">
<!-- Column 1: Width Input -->
<div class="flex-item me-2">
<div class="form-group mb-0">
<input type="number" min="0" class="tiny_imagecms_widthentry form-control me-1 input-mini" id="{{elementid}}_tiny_imagecms_widthentry" value="{{width}}">
<label for="{{elementid}}_tiny_imagecms_widthentry" class="ms-1">{{#str}} width, tiny_mediacms {{/str}}</label>
</div>
</div>
<!-- Column 2: "X" Text -->
<div class="flex-item me-1 mt-2">X</div>
<!-- Column 3: Height Input -->
<div class="flex-item me-1">
<div class="form-group mb-0">
<input type="number" min="0" class="tiny_imagecms_heightentry form-control ms-1 input-mini" id="{{elementid}}_tiny_imagecms_heightentry" value="{{height}}">
<label for="{{elementid}}_tiny_imagecms_heightentry" class="ms-1">{{#str}} height, tiny_mediacms {{/str}}</label>
</div>
</div>
<div class="tiny_imagecms_customhelpicon flex-item ms-1">{{#sizecustomhelpicon}}{{> core/help_icon }}{{/sizecustomhelpicon}}</div>
</div>
</div>
<!-- Row 2: Keep proportion -->
<div class="form-check mb-2">
<input type="checkbox" class="tiny_imagecms_constrain form-check-input" id="{{elementid}}_tiny_imagecms_constrain">
<label class="form-check-label" for="{{elementid}}_tiny_imagecms_constrain">{{#str}} constrain, tiny_mediacms {{/str}}</label>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,42 @@
{{!
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/>.
}}
{{!
@template tiny_mediacms/insert_image_modal_details_footer
Insert image details footer template.
Example context (json):
{
"elementid": "exampleId"
}
}}
<div class="row">
<!-- First Column -->
<div class="col-md-6 d-flex align-items-center p-0">
<!-- Row 1: URL related label -->
<span class="tiny_imagecms_filename text-truncate me-1"></span>
</div>
<!-- Column 2: Saving, canceling, browsing repositories buttons -->
<div class="col-md-6 text-end mt-2 md-0 p-0">
<!-- Row 1: Cancel button -->
<button type="button" class="btn btn-secondary" data-action="cancel">{{#str}} cancel, moodle {{/str}}</button>
<!-- Row 2: Save button -->
<button class="tiny_imagecms_urlentrysubmit btn btn-primary" type="submit">{{#str}} saveimage, tiny_mediacms {{/str}}</button>
</div>
</div>

View File

@@ -0,0 +1,53 @@
{{!
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/>.
}}
{{!
@template tiny_mediacms/insert_image_modal_insert
Insert image body template.
Example context (json):
{
"showdropzone": true,
"showfilepicker": true
}
}}
<div class="tiny_imagecms_insert_image">
<div class="tiny_imagecms_dropzone d-flex flex-column justify-content-center">
{{#showdropzone}}
<div class="tiny_imagecms_dropzone_container"></div>
<!-- File input -->
<input type="file" id="tiny_imagecms_fileinput" accept="image/*" class="d-none">
{{/showdropzone}}
{{^showdropzone}}
<div class="text-center">
<!-- Dropzone icon -->
<i class="fa-6x pb-2 text-secondary fa fa-cloud"></i>
<!-- Dropzone string -->
<div class="lead text-center">
<p class="mb-0">
{{#showfilepicker}}
{{#str}} repositoryuploadnotpermitted, tiny_mediacms {{/str}}</p>
{{/showfilepicker}}
{{^showfilepicker}}
{{#str}} repositorynotpermitted, tiny_mediacms {{/str}}
{{/showfilepicker}}
</div>
</div>
{{/showdropzone}}
</div>
</div>

View File

@@ -0,0 +1,56 @@
{{!
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/>.
}}
{{!
@template tiny_mediacms/insert_image_modal_insert_footer
Insert image footer template.
Example context (json):
{
"showdropzone": true,
"elementid": "exampleId",
"src": "https://moodle.org/logo.png",
"showfilepicker": true
}
}}
<div class="row">
<!-- First Column -->
<div class="col-md-6 d-flex align-items-center p-0">
<!-- Row 1: URL related label -->
{{#showdropzone}}
<label for="{{elementid}}_tiny_imagecms_urlentry" class="tiny_imagecms_url_label me-1">{{#str}} enterurlor, tiny_mediacms {{/str}}</label>
{{/showdropzone}}
{{^showdropzone}}
<label for="{{elementid}}_tiny_imagecms_urlentry" class="tiny_imagecms_url_label me-1">{{#str}} enterurl, tiny_mediacms {{/str}}</label>
{{/showdropzone}}
<!-- Row 2: URL entry input. Needed by the insert image step and image details step if the image URL source from external -->
<input name="urlentry" class="tiny_imagecms_urlentry form-control w-50 me-1" type="url" id="{{elementid}}_tiny_imagecms_urlentry" size="32" value="{{src}}">
<!-- Row 3: Add button. Needed by the insert image step -->
<button disabled class="tiny_imagecms_addurl btn btn-secondary me-1" type="button">{{#str}} addurl, tiny_mediacms {{/str}}</button>
</div>
<!-- Column 2: Saving, canceling, browsing repositories buttons -->
<div class="col-md-6 text-end mt-2 md-0 p-0">
<!-- Row 1: Cancel button -->
<button type="button" class="btn btn-secondary" data-action="cancel">{{#str}} cancel, moodle {{/str}}</button>
{{#showfilepicker}}
<!-- Row 3: Browse repositories button -->
<button class="openimagecmsbrowser btn btn-secondary" type="button">{{#str}} browserepositoriesimage, tiny_mediacms {{/str}}</button>
{{/showfilepicker}}
</div>
</div>

View File

@@ -0,0 +1,34 @@
{{!
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/>.
}}
{{!
@template tiny_mediacms/missingfiles
Insert image template.
Example context (json):
{
"missingFiles": [
"Example.png",
"Another.mov"
]
}
}}
<ol>
{{#missingFiles}}
<li>{{.}}</li>
{{/missingFiles}}
</ol>

View File

@@ -0,0 +1,28 @@
{{!
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/>.
}}
{{!
@template tiny_mediacms/mm2_iframe
Insert image template.
Example context (json):
{
}
}}
<iframe class="mmcms_iframe" id="mmcms-iframe" src={{src}}>
</iframe>

View File

@@ -0,0 +1,74 @@
@editor @editor_tiny @tiny_mediacms @javascript
Feature: Use the TinyMCE editor to upload an image
In order to work with images
As a user
I need to be able to upload and manipulate images
Scenario: Clicking on the Image button in the TinyMCE editor opens the image dialog
Given I log in as "admin"
And I open my profile in edit mode
When I click on the "Image" button for the "Description" TinyMCE editor
Then "Insert image" "dialogue" should exist
Scenario: Browsing repositories in the TinyMCE editor opens the image dialog and shows the FilePicker
Given I log in as "admin"
And I open my profile in edit mode
When I click on the "Image" button for the "Description" TinyMCE editor
And I click on "Browse repositories" "button" in the "Insert image" "dialogue"
Then "File picker" "dialogue" should exist
Scenario: Focus returns to the correct location after closing a nested FilePicker
Given I log in as "admin"
And I open my profile in edit mode
When I click on the "Image" button for the "Description" TinyMCE editor
And I press "Browse repositories"
When I press the escape key
Then the focused element is "Browse repositories" "button"
@_file_upload @test_tiny
Scenario: Browsing repositories in the TinyMCE editor shows the FilePicker and upload url image
Given I log in as "admin"
And I open my profile in edit mode
When I click on the "Image" button for the "Description" TinyMCE editor
And I click on "Browse repositories" "button" in the "Insert image" "dialogue"
And I upload "/lib/editor/tiny/tests/behat/fixtures/tinyscreenshot.png" to the file picker for TinyMCE
# Note: This needs to be replaced with a label.
Then ".tiny_image_preview" "css_element" should be visible
@_file_upload
Scenario: Insert image to the TinyMCE editor
Given I log in as "admin"
And I open my profile in edit mode
And I click on the "Image" button for the "Description" TinyMCE editor
And I click on "Browse repositories" "button" in the "Insert image" "dialogue"
And I upload "lib/editor/tiny/tests/behat/fixtures/moodle-logo.png" to the file picker for TinyMCE
And I set the field "How would you describe this image to someone who can't see it?" to "It's the Moodle"
And I click on "Save" "button" in the "Image details" "dialogue"
When I select the "img" element in position "0" of the "Description" TinyMCE editor
And I click on the "Image" button for the "Description" TinyMCE editor
Then the field "How would you describe this image to someone who can't see it?" matches value "It's the Moodle"
# Note: This needs to be replaced with a label.
And ".tiny_image_preview" "css_element" should be visible
@_file_upload
Scenario: Resizing the image uses the original and custom sizes and the keep proportion checkbox
Given I log in as "admin"
And I open my profile in edit mode
And I click on the "Image" button for the "Description" TinyMCE editor
And I click on "Browse repositories" "button" in the "Insert image" "dialogue"
And I upload "lib/editor/tiny/tests/behat/fixtures/moodle-logo.png" to the file picker for TinyMCE
And I click on "This image is decorative only" "checkbox"
And I click on "Save" "button" in the "Image details" "dialogue"
When I select the "img" element in position "0" of the "Description" TinyMCE editor
And I click on the "Image" button for the "Description" TinyMCE editor
Then the field "Original size" matches value "1"
And I click on "Custom size" "radio"
Then the field "Keep proportion" matches value "1"
And I click on "Keep proportion" "checkbox"
And I set the field "Width" to "102"
And I click on "Save" "button" in the "Image details" "dialogue"
When I select the "img" element in position "0" of the "Description" TinyMCE editor
And I click on the "Image" button for the "Description" TinyMCE editor
Then the field "Custom size" matches value "1"
And the field "Width" matches value "102"
And the field "Keep proportion" matches value "0"

View File

@@ -0,0 +1,58 @@
@editor @editor_tiny @tiny_mediacms @javascript
Feature: Use the TinyMCE editor to upload a video
In order to work with videos
As a user
I need to be able to upload and manipulate videos
Scenario: Clicking on the Video button in the TinyMCE editor opens the video dialog
Given I log in as "admin"
And I open my profile in edit mode
When I click on the "Multimedia" button for the "Description" TinyMCE editor
Then "Insert media" "dialogue" should exist
Scenario: Browsing repositories in the TinyMCE editor shows the FilePicker
Given I log in as "admin"
And I open my profile in edit mode
When I click on the "Multimedia" button for the "Description" TinyMCE editor
And I click on "Browse repositories" "button" in the "Insert media" "dialogue"
Then "File picker" "dialogue" should exist
@_file_upload
Scenario: Browsing repositories in the TinyMCE editor shows the FilePicker
Given I log in as "admin"
And I open my profile in edit mode
When I click on the "Multimedia" button for the "Description" TinyMCE editor
And I follow "Video"
And I click on "Browse repositories..." "button" in the "#id_description_editor_video .tiny_media_source.tiny_media_media_source" "css_element"
And I upload "/lib/editor/tiny/tests/behat/fixtures/moodle-logo.mp4" to the file picker for TinyMCE
When I click on "Insert media" "button"
And I select the "video" element in position "1" of the "Description" TinyMCE editor
@_file_upload
Scenario: Insert and update video in the TinyMCE editor
Given I log in as "admin"
And I open my profile in edit mode
And I click on the "Multimedia" button for the "Description" TinyMCE editor
And I follow "Video"
And I click on "Browse repositories..." "button" in the "#id_description_editor_video .tiny_media_source.tiny_media_media_source" "css_element"
And I upload "/lib/editor/tiny/tests/behat/fixtures/moodle-logo.mp4" to the file picker for TinyMCE
And I click on "Insert media" "button"
And I select the "video" element in position "1" of the "Description" TinyMCE editor
When I click on the "Multimedia" button for the "Description" TinyMCE editor
And I click on "Display options" "link"
And I set the field "Title" to "Test title"
And I click on "Advanced settings" "link"
And I click on "Play automatically" "checkbox"
And I click on "Muted" "checkbox"
And I click on "Loop" "checkbox"
Then "Insert media" "button" should not exist in the "Insert media" "dialogue"
And "Update media" "button" should exist in the "Insert media" "dialogue"
And I click on "Update media" "button"
And I select the "video" element in position "1" of the "Description" TinyMCE editor
And I click on the "Multimedia" button for the "Description" TinyMCE editor
And I click on "Display options" "link"
And the field "Title" matches value "Test title"
And I click on "Advanced settings" "link"
And the field "Play automatically" matches value "1"
And the field "Muted" matches value "1"
And the field "Loop" matches value "1"

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/>.
/**
* Tiny media plugin version details.
*
* @package tiny_media
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2026020200; // Updated 2026-02-02
$plugin->requires = 2024100100;
$plugin->component = 'tiny_mediacms';
$plugin->dependencies = ['filter_mediacms' => 2026020100]; // Keep dependency on our filter