mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-06-06 17:13:02 -04:00
feat: LTI support and Moodle plugin
This commit is contained in:
@@ -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']);
|
||||
};
|
||||
@@ -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
|
||||
|
||||
================================================================================
|
||||
@@ -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
|
||||
Executable
+89
@@ -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 (& -> &) 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';
|
||||
+2457
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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&¤tParsed.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
+10
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3
@@ -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"}
|
||||
+3
@@ -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&§ion.items.unshift(_common.iframeButtonName),section))))};var toolbar,menu}}));
|
||||
|
||||
//# sourceMappingURL=configuration.min.js.map
|
||||
+1
@@ -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"}
|
||||
+3
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3
@@ -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"}
|
||||
+3
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3
@@ -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"}
|
||||
+3
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+10
@@ -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
+3
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
};
|
||||
+118
@@ -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 > Plugins > Filters > 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
@@ -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 |
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
+41
@@ -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):
|
||||
{
|
||||
|
||||
}
|
||||
}}
|
||||
<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>
|
||||
+33
@@ -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>
|
||||
+78
@@ -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 }}
|
||||
+802
@@ -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>
|
||||
+43
@@ -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"/>
|
||||
+832
@@ -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>
|
||||
+50
@@ -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):
|
||||
{
|
||||
|
||||
}
|
||||
}}
|
||||
<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>
|
||||
+129
@@ -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 }}
|
||||
+106
@@ -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>
|
||||
+31
@@ -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>
|
||||
+91
@@ -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
Reference in New Issue
Block a user