feat: LTI support and Moodle plugin

This commit is contained in:
Markos Gogoulos
2026-05-11 12:47:09 +03:00
committed by GitHub
parent b7427869b6
commit 55ab7ff34f
307 changed files with 19966 additions and 3748 deletions
+49
View File
@@ -0,0 +1,49 @@
module.exports = function(grunt) {
var path = require('path');
grunt.initConfig({
babel: {
options: {
sourceMap: true,
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-transform-modules-amd'],
moduleIds: true,
getModuleId: function(moduleName) {
// moduleName is the absolute or relative path to the file.
// We need to convert 'tiny/mediacms/amd/src/filename' to 'tiny_mediacms/filename'
var filename = path.basename(moduleName);
return 'tiny_mediacms/' + filename;
}
},
dist: {
files: [{
expand: true,
cwd: 'tiny/mediacms/amd/src',
src: ['*.js'],
dest: 'tiny/mediacms/amd/build',
ext: '.js'
}]
}
},
uglify: {
options: {
sourceMap: true,
output: { comments: false }
},
dist: {
files: [{
expand: true,
cwd: 'tiny/mediacms/amd/build',
src: ['*.js', '!*.min.js'],
dest: 'tiny/mediacms/amd/build',
ext: '.min.js'
}]
}
}
});
grunt.loadNpmTasks('grunt-babel');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.registerTask('default', ['babel', 'uglify']);
};
+54
View File
@@ -0,0 +1,54 @@
================================================================================
MediaCMS Moodle Plugin Suite v1.0.0
Installation Guide
================================================================================
Requirements
------------
- Moodle 4.5 or later
- MediaCMS instance
- MediaCMS set as External Tool
Installation
------------------
1. Extract zip file to Moodle root public directory:
cd /var/www/moodle/public
unzip mediacms-moodle-v1.0.0.zip
This will place files in:
- filter/mediacms/
- lib/editor/tiny/plugins/mediacms/
2. Set permissions
chown -R www-data:www-data filter/mediacms
chown -R www-data:www-data lib/editor/tiny/plugins/mediacms
3. Install through Moodle
- Log in as Administrator
- Go to: Site Administration → Notifications
- Click "Upgrade Moodle database now"
- Both plugins will be installed automatically
- Set the MediaCMS tool under the LTI Tool
4. Make sure Filter is enabled
- As Administrator, visit Plugins, 'Manage Filters', find MediaCMS filter and enable it.
Then place it at the top of the filter. This is important, otherwise embeds won't load.
What to expect
-------
1. Create a test course
2. Add a page or label
3. Click MediaCMS button in TinyMCE editor
4. Try inserting from video library or pasting a URL
SUPPORT
-------
Issues: https://github.com/mediacms-io/mediacms/issues
Docs: https://docs.mediacms.io
================================================================================
+110
View File
@@ -0,0 +1,110 @@
# MediaCMS for Moodle
Version: 1.0.0, tested on Moodle 5
This plugin provides complete MediaCMS integration for Moodle, consisting of two plugins that work together with unified settings:
1. **Filter Plugin (filter_mediacms):**
* Handles LTI 1.3 authentication and secure video launches
* Auto-converts MediaCMS URLs to embedded players
* **Provides core settings** (MediaCMS URL, LTI Tool ID) used by both plugins
* **Location:** Admin, Plugins, Manage filters, MediaCMS
2. **Editor Plugin (tiny_mediacms):**
* Adds MediaCMS button to TinyMCE editor
* Browse authenticated video library via LTI Deep Linking
* Configure embed options (dimensions, display, start time)
* **Reads core settings** from filter plugin
* **Location:** Admin, Plugins, TinyMCE, MediaCMS
## Installation
Upload the plugin in Moodle's public directory and unzip
# cd /var/www/moodle/public ; cp /root/mediacms-moodle-v1.0.0.zip . && unzip mediacms-moodle-v1.0.0.zip
Ensure the web server user (typically `www-data`) has ownership of the new directories:
```bash
# Example for Ubuntu/Debian systems
chown -R www-data:www-data /var/www/moodle/public/filter/mediacms
chown -R www-data:www-data /var/www/moodle/public/lib/editor/tiny/plugins/mediacms
chmod -R 755 /var/www/moodle/public/filter/mediacms
chmod -R 755 /var/www/moodle/public/lib/editor/tiny/plugins/mediacms
```
### 3. Install Plugins
1. Log in to Moodle as an Administrator.
2. Go to **Site administration > Notifications**.
3. Follow the prompts to upgrade the database and install the new plugins.
## Configuration
### Step 1: Core Settings (Required) - Configure Once
Go to **Site administration > Plugins > Filters > MediaCMS** (Settings)
* **MediaCMS URL:** Enter your MediaCMS instance URL (e.g., `https://lti.mediacms.io`)
* **LTI Tool:** Select the External Tool configuration for MediaCMS
* *First create an LTI 1.3 tool at: Site administration > Plugins > Activity modules > External tool > Manage tools*
> **✨ Note:** These core settings are automatically used by **both** the filter and TinyMCE editor plugin.
### Step 2: Enable Filter
1. Go to **Site administration > Plugins > Filters > Manage filters**
2. Set **MediaCMS** to "On"
### Step 3: Configure Auto-convert Defaults (Optional)
Go to **Site administration > Plugins > Text editors > TinyMCE editor > MediaCMS settings**
Configure default display options for auto-converted URLs:
* Show video title
* Link video title
* Show related videos
* Show user avatar
> **Note:** The core settings (URL, LTI Tool) are managed in the filter plugin settings.
## Usage
### For Teachers (Editor)
1. In any text editor (TinyMCE), click the **MediaCMS** icon (or "Insert MediaCMS Media" from the Insert menu).
2. You can:
* **Paste a URL:** Paste a View or Embed URL.
* **Video Library:** Click the "Video Library" tab to browse and select videos (requires LTI Deep Linking configuration).
3. The video will appear as a placeholder or iframe in the editor.
### For Students (Display)
When content is viewed, the Filter will ensure the video is loaded securely via LTI 1.3, authenticating the user with MediaCMS automatically.
## Build instructions / Developing with the plugin
two types of changes: php (no build), js (build with npx grunt amd)
needs moodle/
npx version, dependencies etc
1. make changes here in lms-plugins/mediacms-moodle
2. copy to moodle
3. run `npx grunt amd` in moodle to build the JS files
4. from moodle copy back
sudo cp -r ~/mediacms/lms-plugins/mediacms-moodle/tiny/mediacms/ -r ~/mediacms/moodle/public/lib/editor/tiny/plugins/
5. cd ~/mediacms/moodle/public/lib/editor/tiny/plugins/mediacms/
npx grunt amd
6.
cp files back...
sudo cp -r /home/user/mediacms/moodle/public/lib/editor/tiny/plugins/mediacms /home/user/mediacms/lms-plugins/mediacms-moodle/tiny/
php admin/cli/purge_caches.php after
### Troubleshooting
Admin, advanced theme settings, add `My Media|/filter/mediacms/my_media.php` in case the position is not workin
+89
View File
@@ -0,0 +1,89 @@
#!/bin/bash
# MediaCMS Moodle Plugin Suite - Build Script
# Creates distributable ZIP package
set -e # Exit on error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}======================================${NC}"
echo -e "${GREEN}MediaCMS Moodle Plugin Suite Builder${NC}"
echo -e "${GREEN}======================================${NC}"
echo
# Configuration
VERSION="1.0.0"
BUILD_DATE=$(date +%Y%m%d)
PACKAGE_NAME="mediacms-moodle-v${VERSION}"
DIST_DIR="dist"
BUILD_DIR="${DIST_DIR}/${PACKAGE_NAME}"
# Create clean dist directory
echo -e "${YELLOW}${NC} Cleaning dist directory..."
rm -rf "${DIST_DIR}"
mkdir -p "${BUILD_DIR}"
# Copy filter plugin
echo -e "${YELLOW}${NC} Copying filter plugin..."
mkdir -p "${BUILD_DIR}/filter"
cp -r filter/mediacms "${BUILD_DIR}/filter/"
# Copy TinyMCE plugin
echo -e "${YELLOW}${NC} Copying TinyMCE plugin..."
mkdir -p "${BUILD_DIR}/lib/editor/tiny/plugins"
cp -r tiny/mediacms "${BUILD_DIR}/lib/editor/tiny/plugins/"
# Copy documentation
echo -e "${YELLOW}${NC} Copying documentation..."
cp README.md "${BUILD_DIR}/filter/mediacms/"
cp INSTALL.txt "${BUILD_DIR}/filter/mediacms/"
# Clean up development files
echo -e "${YELLOW}${NC} Removing development files..."
find "${BUILD_DIR}" -type d -name "node_modules" -exec rm -rf {} + 2>/dev/null || true
find "${BUILD_DIR}" -type f -name ".DS_Store" -delete 2>/dev/null || true
find "${BUILD_DIR}" -type f -name "*.log" -delete 2>/dev/null || true
find "${BUILD_DIR}" -type d -name ".git" -exec rm -rf {} + 2>/dev/null || true
find "${BUILD_DIR}" -type f -name ".gitignore" -delete 2>/dev/null || true
# Remove AMD source files (keep only built versions)
echo -e "${YELLOW}${NC} Cleaning AMD source files..."
find "${BUILD_DIR}/lib/editor/tiny/plugins/mediacms/amd" -type f -name "*.js" ! -name "*-lazy.js" ! -path "*/build/*" -delete 2>/dev/null || true
# Create ZIP archive
echo -e "${YELLOW}${NC} Creating ZIP archive..."
cd "${BUILD_DIR}"
zip -r "../${PACKAGE_NAME}.zip" . -q
cd ../..
# Create checksum
echo -e "${YELLOW}${NC} Generating checksum..."
cd "${DIST_DIR}"
sha256sum "${PACKAGE_NAME}.zip" > "${PACKAGE_NAME}.zip.sha256"
cd ..
# Display results
ZIP_SIZE=$(du -h "${DIST_DIR}/${PACKAGE_NAME}.zip" | cut -f1)
echo
echo -e "${GREEN}✓ Build complete!${NC}"
echo
echo "Package: ${DIST_DIR}/${PACKAGE_NAME}.zip"
echo "Size: ${ZIP_SIZE}"
echo "Checksum: ${DIST_DIR}/${PACKAGE_NAME}.zip.sha256"
echo
echo -e "${YELLOW}Contents:${NC}"
echo " - filter/mediacms/ (includes docs)"
echo " - lib/editor/tiny/plugins/mediacms/"
echo
echo -e "${GREEN}Ready for distribution!${NC}"
echo
# Show checksum
echo -e "${YELLOW}SHA256 Checksum:${NC}"
cat "${DIST_DIR}/${PACKAGE_NAME}.zip.sha256"
echo
Binary file not shown.
@@ -0,0 +1 @@
df913eb7ba0420001b21a522b0841d99209e636073e950f88c93fed4d7e05008 mediacms-moodle-v1.0.0.zip
@@ -0,0 +1,57 @@
<?php
/**
* Hook callbacks for filter_mediacms.
*
* @package filter_mediacms
* @copyright 2026 MediaCMS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace filter_mediacms;
defined('MOODLE_INTERNAL') || die();
class hooks {
/**
* Appends the My Media link to the custom menus after configuration is loaded.
*
* @param \core\hook\after_config $hook
*/
public static function append_my_media_link(\core\hook\after_config $hook): void {
global $CFG;
$navposition = get_config('filter_mediacms', 'mymedia_nav_position');
// MEDIACMS_NAV_PLACEMENT_NONE = 2
if ($navposition == 2) {
return;
}
if (!get_string_manager()->string_exists('mymedia', 'filter_mediacms')) {
return;
}
$linktext = get_string('mymedia', 'filter_mediacms');
$linkurl = '/filter/mediacms/my_media.php';
$menuitem = "\n{$linktext}|{$linkurl}";
// MEDIACMS_NAV_PLACEMENT_PROFILE = 1
if ($navposition == 1) {
// Add to User Profile Menu
if (!isset($CFG->customusermenuitems)) {
$CFG->customusermenuitems = '';
}
if (strpos($CFG->customusermenuitems, $linktext) === false) {
$CFG->customusermenuitems .= $menuitem;
}
} else {
// Default to Top Navigation Menu (MEDIACMS_NAV_PLACEMENT_TOP = 0)
if (!isset($CFG->custommenuitems)) {
$CFG->custommenuitems = '';
}
if (strpos($CFG->custommenuitems, $linktext) === false) {
$CFG->custommenuitems .= $menuitem;
}
}
}
}
@@ -0,0 +1,10 @@
<?php
namespace filter_mediacms\privacy;
defined('MOODLE_INTERNAL') || die();
class provider implements \core_privacy\local\metadata\null_provider {
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,278 @@
<?php
namespace filter_mediacms;
use moodle_url;
use html_writer;
defined('MOODLE_INTERNAL') || die();
/**
* MediaCMS text filter.
*
* @package filter_mediacms
* @copyright 2026 MediaCMS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class text_filter extends \core_filters\text_filter {
/**
* Filter method.
*
* @param string $text The text to filter.
* @param array $options Filter options.
* @return string The filtered text.
*/
public function filter($text, array $options = array()) {
if (!is_string($text) or empty($text)) {
return $text;
}
$mediacmsurl = get_config('filter_mediacms', 'mediacmsurl');
if (empty($mediacmsurl)) {
return $text;
}
$newtext = $text;
// 1. Handle [mediacms:TOKEN] tag
$pattern_tag = '/\[mediacms:([a-zA-Z0-9]+)\]/';
$newtext = preg_replace_callback($pattern_tag, [$this, 'callback_tag'], $newtext);
// 2a. Convert MediaCMS URLs that are already inside <iframe src="..."> attributes
// (saved by the TinyMCE plugin) to launch.php URLs, preserving all other iframe
// attributes. This must run before 2b so the URL pattern below does not try to
// replace just the URL string and produce broken HTML inside the src attribute.
$iframe_src_pattern = '/(<iframe\b[^>]*?\s)src=(["\'])('
. $scheme . ':\/\/' . $host . $path_prefix
. '\/(view|embed)\?m=([a-zA-Z0-9]+)[^"\']*)\2/is';
$newtext = preg_replace_callback($iframe_src_pattern, [$this, 'callback_iframe_src'], $newtext);
// 2b. Auto-convert plain-text MediaCMS URLs to embedded players.
// First, protect text-only links from being converted
// by temporarily replacing them with placeholders.
$textlink_placeholders = [];
$textlink_pattern = '/<a\s+[^>]*data-mediacms-textlink=["\']true["\'][^>]*>.*?<\/a>/is';
$newtext = preg_replace_callback($textlink_pattern, function($matches) use (&$textlink_placeholders) {
$placeholder = '###MEDIACMS_TEXTLINK_' . count($textlink_placeholders) . '###';
$textlink_placeholders[$placeholder] = $matches[0];
return $placeholder;
}, $newtext);
// Regex for plain-text MediaCMS view/embed URLs (not inside iframe src="" — those
// were already handled by 2a above).
$parsed_url = parse_url($mediacmsurl);
$host = preg_quote($parsed_url['host'] ?? '', '/');
$scheme = preg_quote($parsed_url['scheme'] ?? 'https', '/');
// Allow http or https, and optional path prefix
$path_prefix = preg_quote(rtrim($parsed_url['path'] ?? '', '/'), '/');
// Pattern: https://HOST/PREFIX/view?m=TOKEN
// Also handle /embed?m=TOKEN
$pattern_url = '/(' . $scheme . ':\/\/' . $host . $path_prefix . '\/(view|embed)\?m=([a-zA-Z0-9]+)(?:&[^\s<]*)?)/';
$newtext = preg_replace_callback($pattern_url, [$this, 'callback_url'], $newtext);
// Restore protected text-only links as modal launchers
foreach ($textlink_placeholders as $placeholder => $original) {
$newtext = str_replace($placeholder, $this->transform_textlink($original), $newtext);
}
return $newtext;
}
/**
* Callback for MediaCMS URLs found inside existing <iframe src="..."> attributes.
* Replaces only the src value with a launch.php URL so the rest of the iframe
* attributes (width, height, style, etc.) are preserved unchanged.
*
* $matches[1] — everything in the opening tag before `src=`
* $matches[2] — the quote character (" or ')
* $matches[3] — the full MediaCMS URL
* $matches[4] — "view" or "embed"
* $matches[5] — the media friendly_token
*/
public function callback_iframe_src($matches) {
global $COURSE;
$full_url = $matches[3];
$token = $matches[5];
$before_src = $matches[1]; // e.g. '<iframe style="..." '
$quote = $matches[2];
// Extract embed params from the original URL.
$embed_params = [];
$parsed_qs = parse_url($full_url);
if (isset($parsed_qs['query'])) {
// The saved URL may have HTML-entity-encoded ampersands.
$raw_query = html_entity_decode($parsed_qs['query'], ENT_QUOTES | ENT_HTML5);
parse_str($raw_query, $query_params);
foreach (['showTitle', 'showUserAvatar', 'linkTitle', 't', 'width', 'height'] as $p) {
if (isset($query_params[$p])) {
$embed_params[$p] = $query_params[$p];
}
}
}
$launch_params = array_merge(
['token' => $token, 'courseid' => $COURSE->id ?? 0],
$embed_params
);
$launch_url = (new moodle_url('/filter/mediacms/launch.php', $launch_params))->out(false);
// Reconstruct the opening iframe tag with the new src, keeping all other attributes.
return $before_src . 'src=' . $quote . $launch_url . $quote;
}
/**
* Callback for [mediacms:TOKEN]
*/
public function callback_tag($matches) {
return $this->generate_iframe($matches[1], []);
}
/**
* Callback for URLs
*/
public function callback_url($matches) {
// matches[0] is the full matched string
// matches[1] is full URL, matches[3] is token
// Check if this URL is inside a text-only link
// by looking at the context around the match
$fullmatch = $matches[0];
// If this is already inside an <a> tag with data-mediacms-textlink="true",
// return the original URL unchanged
// We'll check this in the main filter method instead
$token = $matches[3];
// Extract additional embed parameters from the URL
$embed_params = [];
$full_url = $matches[1];
// Decode HTML entities (&amp; -> &) before parsing
$full_url = html_entity_decode($full_url, ENT_QUOTES | ENT_HTML5);
$parsed_url = parse_url($full_url);
if (isset($parsed_url['query'])) {
parse_str($parsed_url['query'], $query_params);
// Extract embed-related parameters
$supported_params = ['showTitle', 'showUserAvatar', 'linkTitle', 't', 'width', 'height'];
foreach ($supported_params as $param) {
if (isset($query_params[$param])) {
$embed_params[$param] = $query_params[$param];
}
}
}
return $this->generate_iframe($token, $embed_params);
}
/**
* Generate the Iframe pointing to launch.php
*/
private function generate_iframe($token, $embed_params = []) {
global $CFG, $COURSE;
// Use width/height from embed params if provided, no defaults
$width = isset($embed_params['width']) ? $embed_params['width'] : null;
$height = isset($embed_params['height']) ? $embed_params['height'] : null;
$courseid = $COURSE->id ?? 0;
// Build launch URL parameters
$launch_params = [
'token' => $token,
'courseid' => $courseid
];
// Add width/height only if provided
if ($width !== null) {
$launch_params['width'] = $width;
}
if ($height !== null) {
$launch_params['height'] = $height;
}
// Add other embed parameters if provided (excluding width/height as they're already handled)
foreach ($embed_params as $key => $value) {
if ($key !== 'width' && $key !== 'height') {
$launch_params[$key] = $value;
}
}
$launchurl = new moodle_url('/filter/mediacms/launch.php', $launch_params);
// Build responsive CSS
$max_width = ($width !== null) ? (int)$width : 640;
if ($width !== null && $height !== null && (int)$height > 0) {
$aspect_ratio_css = (int)$width . ' / ' . (int)$height;
} else {
$aspect_ratio_css = '16 / 9';
}
$style = 'width:100%;max-width:' . $max_width . 'px;aspect-ratio:' . $aspect_ratio_css
. ';display:block;margin:0 auto;border:0;';
$iframe_attrs = [
'src' => $launchurl->out(false),
'style' => $style,
'frameborder' => '0',
'allowfullscreen' => 'allowfullscreen',
'title' => 'MediaCMS Video',
];
$iframe = html_writer::tag('iframe', '', $iframe_attrs);
return $iframe;
}
/**
* Transform a text-only link into a link that replaces itself with an inline iframe on click.
*
* @param string $anchor_html Original <a ...>...</a> HTML
* @return string Transformed HTML (or original if token cannot be extracted)
*/
private function transform_textlink($anchor_html) {
global $COURSE;
// Extract href.
if (!preg_match('/href=["\']([^"\']+)["\']/', $anchor_html, $href_matches)) {
return $anchor_html;
}
$href = html_entity_decode($href_matches[1], ENT_QUOTES | ENT_HTML5);
// Extract ?m=TOKEN and optional ?t=seconds.
parse_str(parse_url($href, PHP_URL_QUERY) ?? '', $query_params);
$token = $query_params['m'] ?? null;
if (!$token || !preg_match('/^[a-zA-Z0-9]+$/', $token)) {
return $anchor_html;
}
$start_time = isset($query_params['t']) ? (int)$query_params['t'] : null;
// Extract inner link text.
if (!preg_match('/<a[^>]*>(.*?)<\/a>/is', $anchor_html, $text_matches)) {
return $anchor_html;
}
$courseid = isset($COURSE->id) ? (int)$COURSE->id : 0;
$view_params = ['token' => $token, 'courseid' => $courseid];
if ($start_time !== null && $start_time > 0) {
$view_params['t'] = $start_time;
}
$view_url = new moodle_url('/filter/mediacms/my_media.php', $view_params);
return html_writer::tag('a', $text_matches[1], [
'href' => $view_url->out(false),
'target' => '_blank',
'rel' => 'noopener noreferrer',
]);
}
}
@@ -0,0 +1,18 @@
<?php
/**
* Hook registrations for filter_mediacms.
*
* @package filter_mediacms
* @copyright 2026 MediaCMS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$callbacks = [
[
'hook' => \core\hook\after_config::class,
'callback' => [\filter_mediacms\hooks::class, 'append_my_media_link'],
'priority' => 100,
],
];
@@ -0,0 +1,45 @@
<?php
defined('MOODLE_INTERNAL') || die();
/**
* Post-installation hook.
*/
function xmldb_filter_mediacms_install() {
global $CFG, $DB;
require_once($CFG->libdir . '/filterlib.php');
// Enable the filter globally.
filter_set_global_state('filter_mediacms', TEXTFILTER_ON);
// Move to top priority (lowest sortorder).
$syscontextid = context_system::instance()->id;
$filters = $DB->get_records('filter_active', ['contextid' => $syscontextid], 'sortorder ASC');
if (empty($filters)) {
return;
}
// Separate mediacms from other filters by inspecting the record property,
// not the array key (get_records indexes by id, not by filter name).
$mediacmsrecord = null;
$otherrecords = [];
foreach ($filters as $record) {
if ($record->filter === 'filter_mediacms') {
$mediacmsrecord = $record;
} else {
$otherrecords[] = $record;
}
}
// Reassign sortorders: mediacms first, then everyone else.
$sortorder = 1;
if ($mediacmsrecord) {
$mediacmsrecord->sortorder = $sortorder++;
$DB->update_record('filter_active', $mediacmsrecord);
}
foreach ($otherrecords as $record) {
$record->sortorder = $sortorder++;
$DB->update_record('filter_active', $record);
}
}
@@ -0,0 +1,33 @@
<?php
defined('MOODLE_INTERNAL') || die();
$string['filtername'] = 'MediaCMS';
$string['pluginname'] = 'MediaCMS';
$string['coresettings'] = 'Core MediaCMS Settings';
$string['coresettings_desc'] = 'These settings are shared with the TinyMCE MediaCMS editor plugin.';
$string['mediacmsurl'] = 'MediaCMS URL';
$string['mediacmsurl_desc'] = 'The base URL of your MediaCMS instance (e.g., https://lti.mediacms.io). This setting is used by both the filter and the TinyMCE editor plugin.';
$string['ltitoolid'] = 'LTI Tool';
$string['ltitoolid_desc'] = 'Select the External Tool configuration for MediaCMS. This enables the video library in the TinyMCE editor and LTI authentication. To set up an LTI tool, go to Site Administration > Plugins > Activity modules > External tool > Manage tools.';
$string['noltitoolsfound'] = 'No LTI tools found';
$string['iframewidth'] = 'Default Width';
$string['iframewidth_desc'] = 'Default width for embedded videos (pixels).';
$string['iframeheight'] = 'Default Height';
$string['iframeheight_desc'] = 'Default height for embedded videos (pixels).';
$string['enableautoconvert'] = 'Auto-convert URLs';
$string['enableautoconvert_desc'] = 'Automatically convert MediaCMS URLs (e.g., /view?m=xyz) in text to embedded players.';
$string['privacy:metadata'] = 'The MediaCMS filter does not store any personal data.';
$string['mymedia'] = 'My Media';
$string['notconfigured'] = 'MediaCMS is not fully configured. Please set the MediaCMS URL and LTI Tool in Site Administration → Plugins → Filters → MediaCMS.';
$string['ltitoolnotfound'] = 'The configured LTI tool could not be found. Please check the MediaCMS filter settings.';
$string['cannotcreatedummyactivity'] = 'Could not create the MediaCMS launcher activity. Please check course permissions.';
$string['mymediaposition'] = 'My Media Link Position';
$string['mymediaposition_desc'] = 'Select where the "My Media" link should appear in the Moodle interface.';
$string['pos_topbar'] = 'Top Navigation Bar';
$string['pos_userdrop'] = 'User Profile Dropdown';
$string['pos_none'] = 'None (Do not display)';
$string['shareembeddedmedia'] = 'Share Embedded Media';
$string['shareembeddedmedia_desc'] = 'When enabled, a student viewing embedded media is automatically granted viewer permission on that media (it appears under "Shared with me"). Disable this to allow viewing without creating a sharing record.';
@@ -0,0 +1,169 @@
<?php
/**
* LTI Launch for MediaCMS Filter
*
* @package filter_mediacms
* @copyright 2026 MediaCMS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/mod/lti/lib.php');
require_once($CFG->dirroot . '/mod/lti/locallib.php');
global $SITE, $DB, $PAGE, $OUTPUT, $CFG, $SESSION;
require_login();
$mediatoken = required_param('token', PARAM_ALPHANUMEXT);
$courseid = optional_param('courseid', 0, PARAM_INT);
$height = optional_param('height', 0, PARAM_INT);
$width = optional_param('width', 0, PARAM_INT);
// Extract embed parameters
$showTitle = optional_param('showTitle', '', PARAM_TEXT);
$showUserAvatar = optional_param('showUserAvatar', '', PARAM_TEXT);
$linkTitle = optional_param('linkTitle', '', PARAM_TEXT);
$startTime = optional_param('t', '', PARAM_TEXT);
$show_media_page = optional_param('show_media_page', '', PARAM_TEXT);
// Get configuration
$mediacmsurl = get_config('filter_mediacms', 'mediacmsurl');
$ltitoolid = get_config('filter_mediacms', 'ltitoolid');
$share_raw = get_config('filter_mediacms', 'share_embedded_media');
$share_embedded_media = ($share_raw === false) ? 1 : (int)(bool)$share_raw;
if (empty($mediacmsurl)) {
die('MediaCMS URL not configured');
}
$type = false;
if (!empty($ltitoolid)) {
$type = $DB->get_record('lti_types', ['id' => $ltitoolid]);
}
if (!$type) {
die('LTI tool not found or not configured.');
}
// Set up context
if ($courseid && $courseid != SITEID) {
$context = context_course::instance($courseid);
$course = get_course($courseid);
} else {
$context = context_system::instance();
$course = $SITE;
}
// Build custom params for this video embed.
$custom_params = ["media_friendly_token=" . $mediatoken];
if ($showTitle !== '') {
$custom_params[] = "embed_show_title=" . $showTitle;
}
if ($showUserAvatar !== '') {
$custom_params[] = "embed_show_user_avatar=" . $showUserAvatar;
}
if ($linkTitle !== '') {
$custom_params[] = "embed_link_title=" . $linkTitle;
}
if ($startTime !== '') {
$custom_params[] = "embed_start_time=" . $startTime;
}
if ($show_media_page === 'true') {
$custom_params[] = "show_media_page=true";
}
$custom_params[] = "embed_share_media=" . $share_embedded_media;
// Pass the My Media base URL so MediaCMS can navigate the parent frame back into Moodle
// when the user clicks a media title inside the embed player (see parent_media_base in embeddedApp.ts).
$my_media_base = (new moodle_url('/filter/mediacms/my_media.php'))->out(false);
if ($courseid) {
$my_media_base .= '?courseid=' . intval($courseid);
}
$custom_params[] = "parent_media_base=" . $my_media_base;
// Set up page
$page_params = [
'token' => $mediatoken,
'courseid' => $courseid,
'width' => $width,
'height' => $height
];
if ($showTitle !== '') {
$page_params['showTitle'] = $showTitle;
}
if ($showUserAvatar !== '') {
$page_params['showUserAvatar'] = $showUserAvatar;
}
if ($linkTitle !== '') {
$page_params['linkTitle'] = $linkTitle;
}
if ($startTime !== '') {
$page_params['t'] = $startTime;
}
if ($show_media_page === 'true') {
$page_params['show_media_page'] = 'true';
}
$PAGE->set_url(new moodle_url('/filter/mediacms/launch.php', $page_params));
$PAGE->set_context($context);
$PAGE->set_pagelayout('embedded');
$PAGE->set_title('MediaCMS');
$typeconfig = lti_get_type_type_config($type->id);
// Build the OIDC login request params directly so we can capture the launchid.
// This avoids a shared SESSION key, which would cause a race condition when
// multiple videos are embedded on the same page and load simultaneously.
$oidc_params = lti_build_login_request($course->id, 0, null, $typeconfig, null, 0, 'MediaCMS Video');
// Key the custom params by launchid — lti_auth.php retrieves them the same way.
$hint = json_decode($oidc_params['lti_message_hint']);
$SESSION->{'mediacms_cp_' . $hint->launchid} = implode("\n", $custom_params);
// Build the fallback hidden fields (MediaCMS encodes them in state as a secondary mechanism).
$hidden_fields = '<input type="hidden" name="media_token" value="' . htmlspecialchars($mediatoken, ENT_QUOTES) . '" />';
if ($showTitle !== '') {
$hidden_fields .= '<input type="hidden" name="embed_show_title" value="' . htmlspecialchars($showTitle, ENT_QUOTES) . '" />';
}
if ($showUserAvatar !== '') {
$hidden_fields .= '<input type="hidden" name="embed_show_user_avatar" value="' . htmlspecialchars($showUserAvatar, ENT_QUOTES) . '" />';
}
if ($linkTitle !== '') {
$hidden_fields .= '<input type="hidden" name="embed_link_title" value="' . htmlspecialchars($linkTitle, ENT_QUOTES) . '" />';
}
if ($startTime !== '') {
$hidden_fields .= '<input type="hidden" name="embed_start_time" value="' . htmlspecialchars($startTime, ENT_QUOTES) . '" />';
}
if ($show_media_page === 'true') {
$hidden_fields .= '<input type="hidden" name="show_media_page" value="true" />';
}
if ($width) {
$hidden_fields .= '<input type="hidden" name="embed_width" value="' . htmlspecialchars($width, ENT_QUOTES) . '" />';
}
if ($height) {
$hidden_fields .= '<input type="hidden" name="embed_height" value="' . htmlspecialchars($height, ENT_QUOTES) . '" />';
}
// Produce the OIDC login form (mirrors lti_initiate_login output).
$content = '<form action="' . htmlspecialchars($typeconfig->lti_initiatelogin, ENT_COMPAT)
. '" name="ltiInitiateLoginForm" id="ltiInitiateLoginForm"'
. ' method="post" encType="application/x-www-form-urlencoded">' . "\n";
foreach ($oidc_params as $key => $value) {
$key = htmlspecialchars($key, ENT_COMPAT);
$value = htmlspecialchars($value, ENT_COMPAT);
$content .= " <input type=\"hidden\" name=\"{$key}\" value=\"{$value}\"/>\n";
}
$content .= $hidden_fields . "\n";
$content .= "</form>\n";
$content .= "<script type=\"text/javascript\">\n"
. "//<![CDATA[\n"
. "document.ltiInitiateLoginForm.submit();\n"
. "//]]>\n"
. "</script>\n";
echo $OUTPUT->header();
echo $content;
echo $OUTPUT->footer();
@@ -0,0 +1,7 @@
<?php
defined('MOODLE_INTERNAL') || die();
define('MEDIACMS_NAV_PLACEMENT_TOP', 0);
define('MEDIACMS_NAV_PLACEMENT_PROFILE', 1);
define('MEDIACMS_NAV_PLACEMENT_NONE', 2);
@@ -0,0 +1,10 @@
<?php
/**
* Local helper functions for filter_mediacms
*
* @package filter_mediacms
* @copyright 2026 MediaCMS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
@@ -0,0 +1,196 @@
<?php
/**
* MediaCMS custom LTI 1.3 auth endpoint.
*
* Functionally identical to /mod/lti/auth.php except the no-activity (id=0)
* branch only requires the user to be logged in — not moodle/course:manageactivities.
*
* Custom params (publishdata, redirect_path) are read from the PHP session
* where lti_launch.php / select_media_picker.php store them before starting
* the OIDC flow.
*
* Setup required:
* 1. MediaCMS admin → LTI Platforms → edit Moodle record:
* set "Auth login url" to https://YOUR_MOODLE/filter/mediacms/lti_auth.php
* 2. Moodle admin → Site admin → Plugins → Activity modules → External tool
* → Manage tools → MediaCMS tool → edit → add to "Redirection URIs":
* https://YOUR_MOODLE/filter/mediacms/lti_auth.php
*
* @package filter_mediacms
* @copyright 2026 MediaCMS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/mod/lti/locallib.php');
global $_POST, $_SERVER, $SESSION;
if (!isloggedin() && empty($_POST['repost'])) {
header_remove("Set-Cookie");
$PAGE->set_pagelayout('popup');
$PAGE->set_context(context_system::instance());
$output = $PAGE->get_renderer('mod_lti');
$page = new \mod_lti\output\repost_crosssite_page($_SERVER['REQUEST_URI'], $_POST);
echo $output->header();
echo $output->render($page);
echo $output->footer();
return;
}
$scope = optional_param('scope', '', PARAM_TEXT);
$responsetype = optional_param('response_type', '', PARAM_TEXT);
$clientid = optional_param('client_id', '', PARAM_TEXT);
$redirecturi = optional_param('redirect_uri', '', PARAM_URL);
$loginhint = optional_param('login_hint', '', PARAM_TEXT);
$ltimessagehintenc = optional_param('lti_message_hint', '', PARAM_TEXT);
$state = optional_param('state', '', PARAM_TEXT);
$responsemode = optional_param('response_mode', '', PARAM_TEXT);
$nonce = optional_param('nonce', '', PARAM_TEXT);
$prompt = optional_param('prompt', '', PARAM_TEXT);
$ok = !empty($scope) && !empty($responsetype) && !empty($clientid) &&
!empty($redirecturi) && !empty($loginhint) && !empty($nonce);
if (!$ok) {
$error = 'invalid_request';
}
$ltimessagehint = json_decode($ltimessagehintenc);
$ok = $ok && isset($ltimessagehint->launchid);
if (!$ok) {
$error = 'invalid_request';
$desc = 'No launch id in LTI hint';
}
if ($ok && ($scope !== 'openid')) {
$ok = false;
$error = 'invalid_scope';
}
if ($ok && ($responsetype !== 'id_token')) {
$ok = false;
$error = 'unsupported_response_type';
}
if ($ok) {
$launchid = $ltimessagehint->launchid;
list($courseid, $typeid, $id, $messagetype, $foruserid, $titleb64, $textb64) =
explode(',', $SESSION->$launchid, 7);
unset($SESSION->$launchid);
$config = lti_get_type_type_config($typeid);
$ok = ($clientid === $config->lti_clientid);
if (!$ok) {
$error = 'unauthorized_client';
}
}
if ($ok && ($loginhint !== $USER->id)) {
$ok = false;
$error = 'access_denied';
}
if (empty($config)) {
throw new moodle_exception('invalidrequest', 'error');
} else {
$uris = array_map('trim', explode("\n", $config->lti_redirectionuris));
if (!in_array($redirecturi, $uris)) {
throw new moodle_exception('invalidrequest', 'error');
}
}
if ($ok) {
if (isset($responsemode)) {
$ok = ($responsemode === 'form_post');
if (!$ok) {
$error = 'invalid_request';
$desc = 'Invalid response_mode';
}
} else {
$ok = false;
$error = 'invalid_request';
$desc = 'Missing response_mode';
}
}
if ($ok && !empty($prompt) && ($prompt !== 'none')) {
$ok = false;
$error = 'invalid_request';
$desc = 'Invalid prompt';
}
if ($ok) {
$course = $DB->get_record('course', ['id' => $courseid], '*', MUST_EXIST);
if ($id) {
// Activity-based launch — identical to auth.php's if ($id) branch.
$cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST);
$context = context_module::instance($cm->id);
require_login($course, true, $cm);
require_capability('mod/lti:view', $context);
$lti = $DB->get_record('lti', ['id' => $cm->instance], '*', MUST_EXIST);
$lti->cmid = $cm->id;
list($endpoint, $params) = lti_get_launch_data($lti, $nonce, $messagetype, $foruserid);
} else {
// No-activity launch — student-accessible.
// Custom params (publishdata / redirect_path) were stored in the session
// by lti_launch.php or select_media_picker.php before initiating the OIDC flow.
require_login($course);
// launch.php keys params by launchid (safe for concurrent embeds on one page).
// lti_launch.php and select_media_picker.php use the fixed key (single-use pages).
$customparams = '';
$cpkey = 'mediacms_cp_' . $launchid;
if (!empty($SESSION->$cpkey)) {
$customparams = $SESSION->$cpkey;
unset($SESSION->$cpkey);
} elseif (!empty($SESSION->mediacms_launch_customparams)) {
$customparams = $SESSION->mediacms_launch_customparams;
unset($SESSION->mediacms_launch_customparams);
}
// Minimal LTI instance object — enough for lti_get_launch_data to sign the JWT.
$lti = new stdClass();
$lti->id = 0;
$lti->typeid = (int) $typeid;
$lti->course = (int) $courseid;
$lti->cmid = 0;
$lti->name = 'MediaCMS';
$lti->toolurl = '';
$lti->securetoolurl = '';
$lti->instructorcustomparameters = $customparams;
$lti->instructorchoicesendname = LTI_SETTING_ALWAYS;
$lti->instructorchoicesendemailaddr = LTI_SETTING_ALWAYS;
$lti->instructorchoiceacceptgrades = LTI_SETTING_NEVER;
$lti->instructorchoiceallowroster = null;
$lti->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
$lti->resourcekey = '';
$lti->password = '';
$lti->servicesalt = '';
$lti->resource_link_id = 'mediacms_' . $typeid;
list($endpoint, $params) = lti_get_launch_data(
$lti,
$nonce,
$messagetype ?: 'basic-lti-launch-request',
$foruserid
);
}
} else {
$params['error'] = $error;
if (!empty($desc)) {
$params['error_description'] = $desc;
}
}
if (isset($state)) {
$params['state'] = $state;
}
unset($SESSION->lti_message_hint);
$r = '<form action="' . $redirecturi . "\" name=\"ltiAuthForm\" id=\"ltiAuthForm\" " .
"method=\"post\" enctype=\"application/x-www-form-urlencoded\">\n";
foreach ($params as $key => $value) {
$key = htmlspecialchars($key, ENT_COMPAT);
$value = htmlspecialchars($value, ENT_COMPAT);
$r .= " <input type=\"hidden\" name=\"{$key}\" value=\"{$value}\"/>\n";
}
$r .= "</form>\n";
$r .= "<script type=\"text/javascript\">\n" .
"//<![CDATA[\n" .
"document.ltiAuthForm.submit();\n" .
"//]]>\n" .
"</script>\n";
echo $r;
@@ -0,0 +1,88 @@
<?php
/**
* My Media LTI launch page — runs inside the iframe from my_media.php.
*
* Builds custom_publishdata (enrolled courses + roles) and initiates
* the LTI 1.3 OIDC login flow, outputting the auto-submit form directly.
*
* No dummy LTI activity is created. The publishdata is stored in the PHP
* session and picked up by lti_auth.php during the OIDC callback.
*
* Edge case: if the user is not enrolled in any course the launch still
* proceeds using the site course (SITEID). MediaCMS will receive an empty
* publishdata array and can decide how to handle it (e.g. show a message).
*
* @package filter_mediacms
* @copyright 2026 MediaCMS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/mod/lti/lib.php');
require_once($CFG->dirroot . '/mod/lti/locallib.php');
global $SITE, $DB, $CFG, $USER, $SESSION;
require_login();
$mediacmsurl = get_config('filter_mediacms', 'mediacmsurl');
$ltitoolid = get_config('filter_mediacms', 'ltitoolid');
$share_raw = get_config('filter_mediacms', 'share_embedded_media');
$share_embedded_media = ($share_raw === false) ? 1 : (int)(bool)$share_raw;
if (empty($mediacmsurl) || empty($ltitoolid)) {
throw new moodle_exception('notconfigured', 'filter_mediacms');
}
$type = $DB->get_record('lti_types', ['id' => $ltitoolid]);
if (!$type) {
throw new moodle_exception('ltitoolnotfound', 'filter_mediacms');
}
// Build publishdata: all courses the user is enrolled in + role.
$enrolled_courses = enrol_get_users_courses($USER->id, true, ['id', 'shortname', 'fullname']);
$publish_data = [];
foreach ($enrolled_courses as $enrolled_course) {
if ((int)$enrolled_course->id === SITEID) {
continue;
}
$course_context = context_course::instance($enrolled_course->id);
$roles = get_user_roles($course_context, $USER->id, false);
$role_shortname = 'student';
if (!empty($roles)) {
$role = reset($roles);
$role_shortname = $role->shortname;
}
$publish_data[] = [
'id' => (int)$enrolled_course->id,
'shortname' => $enrolled_course->shortname,
'fullname' => $enrolled_course->fullname,
'role' => $role_shortname,
];
}
$publishdata_b64 = base64_encode(json_encode($publish_data));
// Use a course the user is actually enrolled in so they pass require_login
// in lti_auth.php. Fall back to SITEID for admins with no course enrolments.
$launch_courseid = SITEID;
$launch_course = $SITE;
foreach ($enrolled_courses as $ec) {
if ((int)$ec->id !== SITEID) {
$launch_courseid = (int)$ec->id;
$launch_course = get_course($launch_courseid);
break;
}
}
// Store publishdata in session — lti_auth.php picks it up after the OIDC roundtrip.
$SESSION->mediacms_launch_customparams = 'publishdata=' . $publishdata_b64 . "\nembed_share_media=" . $share_embedded_media;
$typeconfig = lti_get_type_type_config($type->id);
$content = lti_initiate_login($launch_courseid, 0, null, $typeconfig, null, 'MediaCMS My Media');
echo $content;
@@ -0,0 +1,84 @@
<?php
/**
* My Media page — renders the Moodle shell with an LTI iframe.
*
* @package filter_mediacms
* @copyright 2026 MediaCMS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../config.php');
global $SITE, $PAGE, $OUTPUT, $USER, $COURSE;
require_login();
$token = optional_param('token', '', PARAM_ALPHANUMEXT);
$courseid = optional_param('courseid', 0, PARAM_INT);
$start_time = optional_param('t', 0, PARAM_INT);
$context = context_system::instance();
$PAGE->set_context($context);
$PAGE->set_course($SITE);
$PAGE->set_pagelayout('mydashboard');
if ($token) {
$PAGE->set_url(new moodle_url('/filter/mediacms/my_media.php', ['token' => $token]));
$PAGE->set_title('MediaCMS');
$PAGE->set_heading('MediaCMS');
$launch_params = [
'token' => $token,
'courseid' => $courseid ?: ($COURSE->id ?? 0),
'show_media_page' => 'true',
];
if ($start_time > 0) {
$launch_params['t'] = $start_time;
}
$src = (new moodle_url('/filter/mediacms/launch.php', $launch_params))->out(false);
} else {
$PAGE->set_url(new moodle_url('/filter/mediacms/my_media.php'));
$PAGE->set_title(get_string('mymedia', 'filter_mediacms'));
$PAGE->set_heading(get_string('mymedia', 'filter_mediacms'));
$src = (new moodle_url('/filter/mediacms/lti_launch.php'))->out(false);
}
echo $OUTPUT->header();
echo html_writer::tag('iframe', '', [
'id' => 'contentframe',
'src' => $src,
'allowfullscreen' => 'true',
'allow' => 'autoplay *; fullscreen *; encrypted-media *; camera *; microphone *; display-capture *;',
'style' => 'border:none;display:block;width:100%;',
]);
// Fill the iframe to the remaining viewport height and suppress the outer
// page scrollbar. Uses requestAnimationFrame so it runs after Moodle theme
// JS has finished shifting the layout, and re-fires on window load + resize.
echo html_writer::script("
(function () {
var iframe = document.getElementById('contentframe');
function resizeIframe() {
var top = iframe.getBoundingClientRect().top + window.scrollY;
var h = window.innerHeight - iframe.getBoundingClientRect().top;
iframe.style.height = Math.max(h, 100) + 'px';
document.documentElement.style.overflow = 'hidden';
document.body.style.overflow = 'hidden';
}
function schedule() { requestAnimationFrame(resizeIframe); }
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', schedule);
} else {
schedule();
}
window.addEventListener('load', schedule);
window.addEventListener('resize', schedule);
})();
");
echo $OUTPUT->footer();
@@ -0,0 +1,82 @@
<?php
/**
* Student-accessible MediaCMS media picker launcher.
*
* Initiates a no-activity LTI 1.3 OIDC login that routes to MediaCMS's
* /lti/select-media/ UI. No LTI activity is created in the course.
*
* The redirect_path custom param is stored in the PHP session and injected
* by lti_auth.php during the OIDC callback, so MediaCMS routes to the
* media-picker rather than the default My Media page.
*
* Flow:
* 1. TinyMCE plugin opens this URL in an iframe (contentItemUrl).
* 2. We store redirect_path in session and start the OIDC flow.
* 3. lti_auth.php processes the OIDC callback (no manageactivities check).
* 4. MediaCMS receives redirect_path=/lti/select-media/?mode=lms_embed_mode.
* 5. User picks a video; MediaCMS sends postMessage({type:'videoSelected',...})
* which iframeembed.js already handles.
*
* @package filter_mediacms
* @copyright 2026 MediaCMS
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/mod/lti/lib.php');
require_once($CFG->dirroot . '/mod/lti/locallib.php');
global $DB, $PAGE, $OUTPUT, $SITE, $USER, $SESSION;
require_login();
$courseid = required_param('courseid', PARAM_INT);
$action = optional_param('action', '', PARAM_TEXT);
$ltitoolid = get_config('filter_mediacms', 'ltitoolid');
if (empty($ltitoolid)) {
die('MediaCMS LTI tool not configured.');
}
$type = $DB->get_record('lti_types', ['id' => $ltitoolid]);
if (!$type) {
die('LTI tool not found.');
}
// Resolve course — fall back to the user's first enrolled course if needed.
if ($courseid && $courseid != SITEID) {
$course = get_course($courseid);
$context = context_course::instance($courseid);
} else {
$course = $SITE;
$context = context_system::instance();
foreach (enrol_get_users_courses($USER->id, true, ['id']) as $ec) {
if ((int)$ec->id !== SITEID) {
$course = get_course($ec->id);
$context = context_course::instance($ec->id);
break;
}
}
}
require_login($course);
$PAGE->set_url(new moodle_url('/filter/mediacms/select_media_picker.php', ['courseid' => $course->id]));
$PAGE->set_context($context);
$PAGE->set_pagelayout('embedded');
$PAGE->set_title('MediaCMS Select Media');
$typeconfig = lti_get_type_type_config($type->id);
// Store redirect_path in session — lti_auth.php picks it up after the OIDC roundtrip.
if ($action === 'upload') {
$SESSION->mediacms_launch_customparams = 'redirect_path=/upload?action=select_media';
} else {
$SESSION->mediacms_launch_customparams = 'redirect_path=/lti/select-media/?mode=lms_embed_mode';
}
$content = lti_initiate_login($course->id, 0, null, $typeconfig, null, 'MediaCMS Select Media');
echo $OUTPUT->header();
echo $content;
echo $OUTPUT->footer();
@@ -0,0 +1,59 @@
<?php
defined('MOODLE_INTERNAL') || die;
require_once($CFG->dirroot . '/filter/mediacms/lib.php');
if ($ADMIN->fulltree) {
$settings->add(new admin_setting_heading(
'filter_mediacms/coresettings',
get_string('coresettings', 'filter_mediacms'),
get_string('coresettings_desc', 'filter_mediacms')
));
$settings->add(new admin_setting_configselect(
'filter_mediacms/mymedia_nav_position',
get_string('mymediaposition', 'filter_mediacms'),
get_string('mymediaposition_desc', 'filter_mediacms'),
MEDIACMS_NAV_PLACEMENT_TOP,
array(
MEDIACMS_NAV_PLACEMENT_TOP => 'Top Navigation Bar',
MEDIACMS_NAV_PLACEMENT_PROFILE => 'User Profile Dropdown',
MEDIACMS_NAV_PLACEMENT_NONE => 'None (Do not display)'
)
));
$settings->add(new admin_setting_configtext(
'filter_mediacms/mediacmsurl',
get_string('mediacmsurl', 'filter_mediacms'),
get_string('mediacmsurl_desc', 'filter_mediacms'),
'https://lti.mediacms.io',
PARAM_URL
));
$ltioptions = [0 => get_string('noltitoolsfound', 'filter_mediacms')];
try {
$tools = $DB->get_records('lti_types', null, 'name ASC', 'id, name, baseurl');
if (!empty($tools)) {
$ltioptions = [0 => get_string('choose')];
foreach ($tools as $tool) {
$ltioptions[$tool->id] = $tool->name . ' (' . $tool->baseurl . ')';
}
}
} catch (Exception $e) {
}
$settings->add(new admin_setting_configselect(
'filter_mediacms/ltitoolid',
get_string('ltitoolid', 'filter_mediacms'),
get_string('ltitoolid_desc', 'filter_mediacms'),
0,
$ltioptions
));
$settings->add(new admin_setting_configcheckbox(
'filter_mediacms/share_embedded_media',
get_string('shareembeddedmedia', 'filter_mediacms'),
get_string('shareembeddedmedia_desc', 'filter_mediacms'),
1
));
}
@@ -0,0 +1,8 @@
<?php
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2026051100; // 2026-05-11
$plugin->requires = 2024100700; // Requires Moodle 4.5+
$plugin->component = 'filter_mediacms';
$plugin->maturity = MATURITY_STABLE;
$plugin->release = 'v1.0.0';
File diff suppressed because it is too large Load Diff
+89
View File
@@ -0,0 +1,89 @@
{
"name": "mediacms-moodle",
"version": "1.0.0",
"description": "This package provides the integration between MediaCMS and Moodle (versions 4.x and 5.x). It consists of two components that work together to provide a seamless video experience:",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/cli": "^7.28.6",
"@babel/core": "^7.29.0",
"@babel/plugin-transform-modules-amd": "^7.27.1",
"@babel/preset-env": "^7.29.0"
},
"dependencies": {
"anymatch": "^3.1.3",
"babel-plugin-polyfill-corejs2": "^0.4.15",
"babel-plugin-polyfill-corejs3": "^0.14.0",
"babel-plugin-polyfill-regenerator": "^0.6.6",
"balanced-match": "^1.0.2",
"baseline-browser-mapping": "^2.9.19",
"binary-extensions": "^2.3.0",
"brace-expansion": "^1.1.12",
"braces": "^3.0.3",
"browserslist": "^4.28.1",
"caniuse-lite": "^1.0.30001766",
"chokidar": "^3.6.0",
"commander": "^6.2.1",
"concat-map": "^0.0.1",
"convert-source-map": "^2.0.0",
"core-js-compat": "^3.48.0",
"debug": "^4.4.3",
"electron-to-chromium": "^1.5.283",
"escalade": "^3.2.0",
"esutils": "^2.0.3",
"fill-range": "^7.1.1",
"fs-readdir-recursive": "^1.1.0",
"fs.realpath": "^1.0.0",
"function-bind": "^1.1.2",
"gensync": "^1.0.0-beta.2",
"glob": "^7.2.3",
"glob-parent": "^5.1.2",
"hasown": "^2.0.2",
"inflight": "^1.0.6",
"inherits": "^2.0.4",
"is-binary-path": "^2.1.0",
"is-core-module": "^2.16.1",
"is-extglob": "^2.1.1",
"is-glob": "^4.0.3",
"is-number": "^7.0.0",
"js-tokens": "^4.0.0",
"jsesc": "^3.1.0",
"json5": "^2.2.3",
"lodash.debounce": "^4.0.8",
"lru-cache": "^5.1.1",
"make-dir": "^2.1.0",
"minimatch": "^3.1.2",
"ms": "^2.1.3",
"node-releases": "^2.0.27",
"normalize-path": "^3.0.0",
"once": "^1.4.0",
"path-is-absolute": "^1.0.1",
"path-parse": "^1.0.7",
"picocolors": "^1.1.1",
"picomatch": "^2.3.1",
"pify": "^4.0.1",
"readdirp": "^3.6.0",
"regenerate": "^1.4.2",
"regenerate-unicode-properties": "^10.2.2",
"regexpu-core": "^6.4.0",
"regjsgen": "^0.8.0",
"regjsparser": "^0.13.0",
"resolve": "^1.22.11",
"semver": "^6.3.1",
"slash": "^2.0.0",
"supports-preserve-symlinks-flag": "^1.0.0",
"to-regex-range": "^5.0.1",
"unicode-canonical-property-names-ecmascript": "^2.0.1",
"unicode-match-property-ecmascript": "^2.0.0",
"unicode-match-property-value-ecmascript": "^2.2.1",
"unicode-property-aliases-ecmascript": "^2.2.0",
"update-browserslist-db": "^1.2.3",
"wrappy": "^1.0.2",
"yallist": "^3.1.1"
}
}
+206
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
@@ -0,0 +1,20 @@
# TinyMCE MediaCMS Plugin for Moodle
A TinyMCE editor plugin for Moodle that provides media embedding capabilities with MediaCMS/LTI integration.
## Build Information
### Preparation
1. Get and extract Moodle 5.1
2. cp -r lms-plugins/mediacms-moodle/tiny/mediacms/ moodle/public/lib/editor/tiny/plugins/
3. nvm use 22 && cd moodle/public && npm install
### Actual build
4. cd lib/editor/tiny/plugins/mediacms && npx grunt amd
### Test the output
5. To test the output:
cp * ../../../../../../../lms-plugins/mediacms-moodle/tiny/mediacms/ -r
6. Then copy to Moodle server and purge caches
+15
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("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
+3
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
@@ -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"}
@@ -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
@@ -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
+3
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
@@ -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
+3
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
@@ -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
+10
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
+3
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
@@ -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"}
+3
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
@@ -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"}
+11
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,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
@@ -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 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","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,gBAAgB,MAK5Bd,eAAeF,QAAS,CACpBK,UAAW,iBACA,CAEPY,OAAQ,EACRC,SAAU,EACVC,eAAgB,aAWfC,eAAkBnB,QAAWA,OAAOE,QAAQkB,IAAItB,qFAQzBE,QAAWmB,eAAenB,QAAQK,mCAQlCL,QAAWmB,eAAenB,QAAQqB,uBAQ9CrB,QAAWA,OAAOE,QAAQkB,IAAIxB,0BAQ/BI,QAAWA,OAAOE,QAAQkB,IAAIrB"}
+10
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}},convertUrlsToIframes=html=>{const tempDiv=document.createElement("div");tempDiv.innerHTML=html;const nodesToReplace=[],walk=el=>{for(const child of Array.from(el.childNodes))if(child.nodeType===Node.TEXT_NODE){const url=child.textContent.trim();isMediaCMSUrl(url)&&nodesToReplace.push({node:child,url:url})}else child.nodeType===Node.ELEMENT_NODE&&"a"!==child.tagName.toLowerCase()&&walk(child)};return walk(tempDiv),nodesToReplace.forEach((_ref=>{let{node:node,url:url}=_ref;const wrapper=document.createElement("div");wrapper.innerHTML=(url=>{let embedUrl=url,width=560,height=315;try{const urlObj=new URL(url);"/view"===urlObj.pathname&&(urlObj.pathname="/embed");const w=parseInt(urlObj.searchParams.get("width")),h=parseInt(urlObj.searchParams.get("height"));w>0&&(width=w),h>0&&(height=h),embedUrl=urlObj.toString()}catch(e){}const style="width:100%;max-width:".concat(width,"px;height:auto;")+"aspect-ratio:".concat(width," / ").concat(height,";display:block;margin:0 auto;border:0;");return'<iframe src="'.concat(embedUrl,'" width="').concat(width,'" height="').concat(height,'" ')+'style="'.concat(style,'" frameborder="0" allowfullscreen></iframe>')})(url);const iframe=wrapper.firstChild;iframe&&node.parentNode.replaceChild(iframe,node)})),tempDiv.innerHTML};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=convertUrlsToIframes(e.content))})),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"),p=document.createElement("p");p.appendChild(document.createTextNode(src)),wrapper?(wrapper.parentNode.insertBefore(p,wrapper),wrapper.remove()):(iframe.parentNode.insertBefore(p,iframe),iframe.remove())}})),tempDiv.querySelectorAll(".tiny-mediacms-iframe-wrapper, .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
+3
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",showUserAvatar:".tiny_iframecms_showuseravatar",textLinkOnly:".tiny_iframecms_textlinkonly",startAt:".tiny_iframecms_startat",startAtEnabled:".tiny_iframecms_startat_enabled",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",tabUploadMediaBtn:".tiny_iframecms_upload_media_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"}}},_exports.default}));
//# sourceMappingURL=selectors.min.js.map
File diff suppressed because one or more lines are too long
+10
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
@@ -0,0 +1,263 @@
// 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('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);
};
@@ -0,0 +1,308 @@
// 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')) ||
(node.nodeName.toLowerCase() === 'a' && node.getAttribute('data-mediacms-textlink') === 'true');
/**
* 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 fixWrapperWidths = () => {
const editorBody = editor.getBody();
if (!editorBody) {
return;
}
editorBody.querySelectorAll('.tiny-mediacms-iframe-wrapper').forEach((wrapper) => {
const iframe = wrapper.querySelector('iframe');
if (!iframe) {
return;
}
const iframeStyle = iframe.getAttribute('style') || '';
const match = iframeStyle.match(/max-width:\s*(\d+(?:\.\d+)?)px/);
if (match) {
wrapper.style.maxWidth = match[1] + 'px';
wrapper.style.width = '100%';
wrapper.style.margin = '0 auto';
}
});
};
const processIframes = () => {
const editorBody = editor.getBody();
if (!editorBody) {
return;
}
const iframes = editorBody.querySelectorAll('iframe');
iframes.forEach((iframe) => {
// If already wrapped, ensure contenteditable and EDIT button are present
if (iframe.parentElement?.classList.contains('tiny-mediacms-iframe-wrapper')) {
const existingWrapper = iframe.parentElement;
existingWrapper.setAttribute('contenteditable', 'false');
if (!existingWrapper.querySelector('.tiny-mediacms-edit-btn')) {
const editBtn = editor.getDoc().createElement('button');
editBtn.className = 'tiny-mediacms-edit-btn';
editBtn.setAttribute('type', 'button');
editBtn.setAttribute('title', 'Edit media embed options');
editBtn.textContent = 'EDIT';
existingWrapper.appendChild(editBtn);
}
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 media embed options');
// Use text "EDIT" instead of icon
editBtn.textContent = 'EDIT';
// Wrap the iframe: insert wrapper, move iframe into it, add button
iframe.parentNode.insertBefore(wrapper, iframe);
wrapper.appendChild(iframe);
wrapper.appendChild(editBtn);
});
fixWrapperWidths();
};
/**
* 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;
margin-top: 28px;
}
.tiny-mediacms-iframe-wrapper iframe {
display: block;
}
.tiny-mediacms-edit-btn {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.7);
color: #ffffff;
border: none;
border-radius: 3px;
cursor: pointer;
z-index: 10;
padding: 8px 20px;
margin: 0;
font-size: 14px;
font-weight: bold;
text-decoration: none;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
transition: background 0.15s, box-shadow 0.15s;
display: inline-block;
box-sizing: border-box;
}
.tiny-mediacms-edit-btn:hover {
background: rgba(0, 0, 0, 0.85);
box-shadow: 0 3px 6px rgba(0,0,0,0.4);
}
`;
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 => {
const selector = [
'iframe:not([data-mce-object]):not([data-mce-placeholder])',
'.tiny-iframe-responsive',
'.tiny-mediacms-iframe-wrapper',
'a[data-mediacms-textlink="true"]'
].join(',');
return editor.selection.selectorChangedWithUnbind(
selector,
api.setActive
).unbind;
}
});
editor.ui.registry.addMenuItem(iframeMenuItemName, {
icon: iframeIcon,
text: iframeButtonText,
onAction: handleIframeAction,
});
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);
};
};
@@ -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',
};
@@ -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),
};
};
+467
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;
}
});
});
}
}
@@ -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
@@ -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);
}
}
+273
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);
}
}
@@ -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,
};
};
}
@@ -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+%/);
};
@@ -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);
});
}
}
}
@@ -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();
@@ -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();
}
}
@@ -0,0 +1,116 @@
// 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,
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);
+199
View File
@@ -0,0 +1,199 @@
// 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) => {
let embedUrl = url;
let width = 560;
let height = 315;
try {
const urlObj = new URL(url);
if (urlObj.pathname === '/view') {
urlObj.pathname = '/embed';
}
const w = parseInt(urlObj.searchParams.get('width'));
const h = parseInt(urlObj.searchParams.get('height'));
if (w > 0) {
width = w;
}
if (h > 0) {
height = h;
}
embedUrl = urlObj.toString();
} catch (e) {
// Keep defaults if parsing fails
}
const style = `width:100%;max-width:${width}px;height:auto;` +
`aspect-ratio:${width} / ${height};display:block;margin:0 auto;border:0;`;
return `<iframe src="${embedUrl}" width="${width}" height="${height}" ` +
`style="${style}" frameborder="0" allowfullscreen></iframe>`;
};
/**
* Convert standalone MediaCMS URL text nodes to iframes.
* Uses DOM traversal so URLs inside <a> tags (text links) are never touched.
*
* @param {string} html - Raw HTML string from the editor
* @returns {string} HTML with standalone URLs replaced by iframe HTML
*/
const convertUrlsToIframes = (html) => {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
const nodesToReplace = [];
const walk = (el) => {
for (const child of Array.from(el.childNodes)) {
if (child.nodeType === Node.TEXT_NODE) {
const url = child.textContent.trim();
if (isMediaCMSUrl(url)) {
nodesToReplace.push({node: child, url});
}
} else if (child.nodeType === Node.ELEMENT_NODE && child.tagName.toLowerCase() !== 'a') {
walk(child);
}
// Do not recurse into <a> tags — text links must be preserved as-is
}
};
walk(tempDiv);
nodesToReplace.forEach(({node, url}) => {
const wrapper = document.createElement('div');
wrapper.innerHTML = mediaCMSUrlToIframe(url);
const iframe = wrapper.firstChild;
if (iframe) {
node.parentNode.replaceChild(iframe, node);
}
});
return tempDiv.innerHTML;
};
// 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 standalone MediaCMS URL text nodes to iframes when loading content.
// Text links (<a data-mediacms-textlink>) are preserved because DOM traversal skips <a> tags.
editor.on('BeforeSetContent', (e) => {
if (e.content && typeof e.content === 'string') {
e.content = convertUrlsToIframes(e.content);
}
});
// Convert MediaCMS iframes back to plain embed URLs when saving.
// Width/height are encoded in the URL params so the filter and BeforeSetContent
// can reconstruct the correct responsive iframe on next load.
editor.on('GetContent', (e) => {
if (e.format === 'html') {
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');
const p = document.createElement('p');
p.appendChild(document.createTextNode(src));
if (wrapper) {
wrapper.parentNode.insertBefore(p, wrapper);
wrapper.remove();
} else {
iframe.parentNode.insertBefore(p, iframe);
iframe.remove();
}
}
});
tempDiv.querySelectorAll('.tiny-mediacms-iframe-wrapper, .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]);
});
@@ -0,0 +1,155 @@
// 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',
showUserAvatar: '.tiny_iframecms_showuseravatar',
textLinkOnly: '.tiny_iframecms_textlinkonly',
startAt: '.tiny_iframecms_startat',
startAtEnabled: '.tiny_iframecms_startat_enabled',
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',
tabUploadMediaBtn: '.tiny_iframecms_upload_media_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',
},
},
};
@@ -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;
};
@@ -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,
]);
}
}
@@ -0,0 +1,224 @@
<?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 {
// Read MediaCMS base URL from filter settings (unified)
$baseurl = get_config('filter_mediacms', 'mediacmsurl');
// 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'),
'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;
// Read LTI tool ID from filter settings (unified)
$ltitoolid = get_config('filter_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 URL for the student-accessible media picker.
// Uses /filter/mediacms/select_media_picker.php instead of the standard
// /mod/lti/contentitem.php, which requires moodle/course:manageactivities
// and therefore fails for students.
$contentitemurl = '';
if (!empty($ltitoolid) && $courseid > 0) {
$contentitemurl = (new moodle_url('/filter/mediacms/select_media_picker.php', [
'courseid' => $courseid,
]))->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,
],
];
}
}
@@ -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';
}
}
@@ -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
@@ -0,0 +1,189 @@
<?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['showuseravatar'] = 'Show user avatar';
$string['responsive'] = 'Responsive';
$string['textlinkonly'] = 'Insert text link only';
$string['startat'] = 'Start at';
$string['dimensions'] = 'Dimensions';
$string['preview'] = 'Preview';
$string['insertiframe'] = 'Insert';
$string['updateiframe'] = 'Update video';
$string['removeiframe'] = 'Remove video';
$string['removeiframeconfirm'] = 'Are you sure you want to remove this video from the editor?';
// Iframe modal tabs.
$string['tabembedurl'] = 'Configure';
$string['tabvideolibrary'] = 'Video Library';
$string['tabvideolibraryiframe'] = 'My Media';
// 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...';
$string['libraryempty'] = 'Not found';
$string['libraryerror'] = 'Failed to load';
$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.';
// Core settings (configured in filter plugin).
$string['coresettingsheading'] = 'Core MediaCMS Settings';
$string['coresettingsheading_desc'] = '<strong>Note:</strong> MediaCMS URL and LTI Tool configuration are managed in the filter plugin. Go to: <a href="../filter/manage.php">Site Administration &gt; Plugins &gt; Filters &gt; Manage filters</a>, then click "Settings" next to MediaCMS.';
// 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_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';
+148
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();
@@ -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

+4
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

+62
View File
@@ -0,0 +1,62 @@
<?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) {
// Note: Core MediaCMS settings (URL, LTI Tool) are configured in the filter plugin
// Go to: Site Administration > Plugins > Filters > MediaCMS
$settings->add(new admin_setting_heading(
'tiny_mediacms/coresettingsheading',
new lang_string('coresettingsheading', 'tiny_mediacms'),
new lang_string('coresettingsheading_desc', 'tiny_mediacms')
));
// Editor-specific settings: Auto-convert default options
// 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_showuseravatar',
new lang_string('autoconvert_showuseravatar', 'tiny_mediacms'),
new lang_string('autoconvert_showuseravatar_desc', 'tiny_mediacms'),
1
);
$settings->add($setting);
}
+83
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;
}
}
@@ -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;
@@ -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>
@@ -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 }}
@@ -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>
@@ -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"/>
@@ -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>
@@ -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;
@@ -0,0 +1,129 @@
{{!
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">
<!-- My Media tab (first, active by default when not editing) -->
<li class="nav-item" role="presentation">
<button class="nav-link {{^isupdating}}active{{/isupdating}} 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="{{^isupdating}}true{{/isupdating}}{{#isupdating}}false{{/isupdating}}">
{{#str}} tabvideolibraryiframe, tiny_mediacms {{/str}}
</button>
</li>
<!-- Configure tab (second, hidden initially when inserting, visible when editing) -->
<li class="nav-item tiny_iframecms_tab_url_item" role="presentation" {{^isupdating}}style="display: none;"{{/isupdating}}>
<button class="nav-link {{#isupdating}}active{{/isupdating}} 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="{{#isupdating}}true{{/isupdating}}{{^isupdating}}false{{/isupdating}}">
{{#str}} tabembedurl, tiny_mediacms {{/str}}
</button>
</li>
<!-- Upload media button -->
<li class="nav-item ms-auto ml-auto" role="presentation">
<button class="btn btn-outline-secondary btn-sm mt-1 me-2 mr-2 tiny_iframecms_upload_media_btn" type="button" title="Upload media">
<svg xmlns="http://www.w3.org/2000/svg" height="18px" viewBox="0 -960 960 960" width="18px" fill="currentColor" style="vertical-align: text-bottom; margin-right: 4px;"><path d="M440-440H280v-80h160v-160h80v160h160v80H520v160h-80v-160ZM160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h480q33 0 56.5 23.5T720-720v180l160-160v440L720-420v180q0 33-23.5 56.5T640-160H160Zm0-80h480v-480H160v480Zm0 0v-480 480Z"/></svg>Upload media
</button>
</li>
</ul>
<!-- Tab Content -->
<div class="tab-content">
<!-- Tab 1: My Media (active when not editing) -->
<div class="tab-pane fade {{^isupdating}}show active{{/isupdating}} 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>
<!-- Tab 2: Configure (active when editing) -->
<div class="tab-pane fade {{#isupdating}}show active{{/isupdating}} tiny_iframecms_pane_url" id="{{elementid}}_pane_url" role="tabpanel" aria-labelledby="{{elementid}}_tab_url">
<div class="container-fluid p-0">
<div class="row">
<!-- Left column: Options only (URL field removed) -->
<div class="col-md-6">
<!-- Hidden URL input (still needed for internal logic) -->
<textarea
class="form-control tiny_iframecms_url d-none"
id="{{elementid}}_iframe_url"
rows="3"
>{{url}}</textarea>
<div class="tiny_iframecms_url_warning text-danger small mt-1 d-none"></div>
{{> tiny_mediacms/iframe_embed_options }}
</div>
<!-- Right column: Preview -->
<div class="col-md-6">
<label class="form-label font-weight-bold">
{{#str}} preview, tiny_mediacms {{/str}}
</label>
<div class="tiny_iframecms_preview_container border rounded p-2 bg-light" style="min-height: 300px;">
<div class="tiny_iframecms_preview d-flex align-items-center justify-content-center text-muted" style="min-height: 280px;">
<span>{{#str}} iframeurlplaceholder, tiny_mediacms {{/str}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
{{/body}}
{{$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>
<button type="button" class="btn btn-secondary" data-action="cancel">{{#str}} cancel, moodle {{/str}}</button>
{{/footer}}
{{/ core/modal }}
@@ -0,0 +1,106 @@
{{!
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,
"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_textlinkonly"
id="{{elementid}}_textlinkonly" {{#textLinkOnly}}checked{{/textLinkOnly}}>
<label class="form-check-label" for="{{elementid}}_textlinkonly">
{{#str}} textlinkonly, 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 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>
<!-- 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" min="1" step="1">
<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" min="1" step="1">
<span class="input-group-text">px</span>
</div>
<small class="text-muted">{{#str}} height, tiny_mediacms {{/str}}</small>
</div>
</div>
</div>
@@ -0,0 +1,31 @@
{{!
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"
}
}}
<div class="tiny-mediacms-iframe-wrapper" contenteditable="false" style="max-width:{{maxWidth}}px;width:100%;margin:0 auto;padding:0;"><iframe src="{{src}}" width="{{maxWidth}}" height="{{height}}" style="width:100%;max-width:{{maxWidth}}px;height:auto;aspect-ratio:{{aspectRatioCSS}};display:block;margin:0 auto;border:0;" frameborder="0" allowfullscreen></iframe></div>
@@ -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>

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