diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/AUTOCONVERT.md b/lms-plugins/mediacms-moodle/tiny/mediacms/AUTOCONVERT.md new file mode 100755 index 00000000..d1ac4526 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/AUTOCONVERT.md @@ -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 +
+ +
+``` + +## 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 diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/autoconvert.min.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/autoconvert.min.js new file mode 100755 index 00000000..f310b679 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/autoconvert.min.js @@ -0,0 +1,15 @@ +define("tiny_mediacms/autoconvert",["exports","./options"],(function(_exports,_options){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setupAutoConvert=_exports.isMediaCMSUrl=_exports.convertToEmbed=void 0; +/** + * Tiny MediaCMS Auto-convert module. + * + * This module automatically converts pasted MediaCMS URLs into embedded videos. + * When a user pastes a MediaCMS video URL (e.g., https://deic.mediacms.io/view?m=JpBd1Zvdl), + * it will be automatically converted to an iframe embed. + * + * @module tiny_mediacms/autoconvert + * @copyright 2024 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +const MEDIACMS_VIEW_URL_PATTERN=/^(https?:\/\/[^\/]+)\/view\?m=([a-zA-Z0-9_-]+)$/,parseMediaCMSUrl=text=>{if(!text||"string"!=typeof text)return null;const trimmed=text.trim(),match=trimmed.match(MEDIACMS_VIEW_URL_PATTERN);return match?{baseUrl:match[1],videoId:match[2],originalUrl:trimmed}:null},isDomainAllowed=(parsed,config)=>{const configuredBaseUrl=config.autoConvertBaseUrl||config.mediacmsBaseUrl;if(!configuredBaseUrl)return!0;try{const configuredUrl=new URL(configuredBaseUrl),pastedUrl=new URL(parsed.baseUrl);return configuredUrl.host===pastedUrl.host}catch(e){return!0}},generateEmbedHtml=function(parsed){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const embedUrl=new URL("".concat(parsed.baseUrl,"/embed"));embedUrl.searchParams.set("m",parsed.videoId),embedUrl.searchParams.set("showTitle",!1!==options.showTitle?"1":"0"),embedUrl.searchParams.set("showRelated",!1!==options.showRelated?"1":"0"),embedUrl.searchParams.set("showUserAvatar",!1!==options.showUserAvatar?"1":"0"),embedUrl.searchParams.set("linkTitle",!1!==options.linkTitle?"1":"0");const html='';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 \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/autoconvert.min.js.map b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/autoconvert.min.js.map new file mode 100755 index 00000000..36a38568 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/autoconvert.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"autoconvert.min.js","sources":["../src/autoconvert.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 .\n\n/**\n * Tiny MediaCMS Auto-convert module.\n *\n * This module automatically converts pasted MediaCMS URLs into embedded videos.\n * When a user pastes a MediaCMS video URL (e.g., https://deic.mediacms.io/view?m=JpBd1Zvdl),\n * it will be automatically converted to an iframe embed.\n *\n * @module tiny_mediacms/autoconvert\n * @copyright 2024\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getData} from './options';\n\n/**\n * Regular expression patterns for MediaCMS URLs.\n * Matches URLs like:\n * - https://deic.mediacms.io/view?m=JpBd1Zvdl\n * - https://example.mediacms.io/view?m=VIDEO_ID\n * - Custom domains configured in the plugin\n */\nconst MEDIACMS_VIEW_URL_PATTERN = /^(https?:\\/\\/[^\\/]+)\\/view\\?m=([a-zA-Z0-9_-]+)$/;\n\n/**\n * Check if a string is a valid MediaCMS view URL.\n *\n * @param {string} text - The text to check\n * @returns {Object|null} - Parsed URL info or null if not a valid MediaCMS URL\n */\nconst parseMediaCMSUrl = (text) => {\n if (!text || typeof text !== 'string') {\n return null;\n }\n\n const trimmed = text.trim();\n\n // Check for MediaCMS view URL pattern\n const match = trimmed.match(MEDIACMS_VIEW_URL_PATTERN);\n if (match) {\n return {\n baseUrl: match[1],\n videoId: match[2],\n originalUrl: trimmed,\n };\n }\n\n return null;\n};\n\n/**\n * Check if the pasted URL's domain is allowed based on configuration.\n *\n * @param {Object} parsed - Parsed URL info\n * @param {Object} config - Plugin configuration\n * @returns {boolean} - True if the domain is allowed\n */\nconst isDomainAllowed = (parsed, config) => {\n // If no specific base URL is configured, allow all MediaCMS domains\n const configuredBaseUrl = config.autoConvertBaseUrl || config.mediacmsBaseUrl;\n if (!configuredBaseUrl) {\n return true;\n }\n\n // Check if the URL's base matches the configured base URL\n try {\n const configuredUrl = new URL(configuredBaseUrl);\n const pastedUrl = new URL(parsed.baseUrl);\n return configuredUrl.host === pastedUrl.host;\n } catch (e) {\n // If URL parsing fails, allow the conversion\n return true;\n }\n};\n\n/**\n * Generate the iframe embed HTML for a MediaCMS video.\n *\n * @param {Object} parsed - Parsed URL info\n * @param {Object} options - Embed options\n * @returns {string} - The iframe HTML\n */\nconst generateEmbedHtml = (parsed, options = {}) => {\n // Build the embed URL with default options\n const embedUrl = new URL(`${parsed.baseUrl}/embed`);\n embedUrl.searchParams.set('m', parsed.videoId);\n\n // Apply default options (all enabled by default for best user experience)\n embedUrl.searchParams.set('showTitle', options.showTitle !== false ? '1' : '0');\n embedUrl.searchParams.set('showRelated', options.showRelated !== false ? '1' : '0');\n embedUrl.searchParams.set('showUserAvatar', options.showUserAvatar !== false ? '1' : '0');\n embedUrl.searchParams.set('linkTitle', options.linkTitle !== false ? '1' : '0');\n\n // Generate clean iframe HTML (wrapper will be added by editor for UI, then stripped on save)\n const html = ``;\n\n return html;\n};\n\n/**\n * Set up auto-conversion for the editor.\n * This registers event handlers to detect pasted MediaCMS URLs.\n *\n * @param {TinyMCE} editor - The TinyMCE editor instance\n */\nexport const setupAutoConvert = (editor) => {\n const config = getData(editor) || {};\n\n // Check if auto-convert is enabled (default: true)\n if (config.autoConvertEnabled === false) {\n return;\n }\n\n // Handle paste events\n editor.on('paste', (e) => {\n handlePasteEvent(editor, e, config);\n });\n\n // Also handle input events for drag-and-drop text or keyboard paste\n editor.on('input', (e) => {\n handleInputEvent(editor, e, config);\n });\n};\n\n/**\n * Handle paste events to detect and convert MediaCMS URLs.\n *\n * @param {TinyMCE} editor - The TinyMCE editor instance\n * @param {Event} e - The paste event\n * @param {Object} config - Plugin configuration\n */\nconst handlePasteEvent = (editor, e, config) => {\n // Get pasted text from clipboard\n const clipboardData = e.clipboardData || window.clipboardData;\n if (!clipboardData) {\n return;\n }\n\n // Try to get plain text first\n const text = clipboardData.getData('text/plain') || clipboardData.getData('text');\n if (!text) {\n return;\n }\n\n // Check if it's a MediaCMS URL\n const parsed = parseMediaCMSUrl(text);\n if (!parsed) {\n return;\n }\n\n // Check if domain is allowed\n if (!isDomainAllowed(parsed, config)) {\n return;\n }\n\n // Prevent default paste behavior\n e.preventDefault();\n e.stopPropagation();\n\n // Generate and insert the embed HTML\n const embedHtml = generateEmbedHtml(parsed, config.autoConvertOptions || {});\n\n // Use a slight delay to ensure the editor is ready\n setTimeout(() => {\n editor.insertContent(embedHtml);\n // Move cursor after the inserted content\n editor.selection.collapse(false);\n }, 0);\n};\n\n/**\n * Handle input events to catch URLs that might have been pasted without triggering paste event.\n * This is a fallback for certain browsers/scenarios.\n *\n * @param {TinyMCE} editor - The TinyMCE editor instance\n * @param {Event} e - The input event\n * @param {Object} config - Plugin configuration\n */\nconst handleInputEvent = (editor, e, config) => {\n // Only process inputType 'insertFromPaste' if paste event didn't catch it\n if (e.inputType !== 'insertFromPaste' && e.inputType !== 'insertText') {\n return;\n }\n\n // Get the current node and check if it contains just a URL\n const node = editor.selection.getNode();\n if (!node || node.nodeName !== 'P') {\n return;\n }\n\n // Check if the paragraph contains only a MediaCMS URL\n const text = node.textContent || '';\n const parsed = parseMediaCMSUrl(text);\n\n if (!parsed || !isDomainAllowed(parsed, config)) {\n return;\n }\n\n // Don't convert if there's other content in the paragraph\n const trimmedHtml = node.innerHTML.trim();\n if (trimmedHtml !== text.trim() && !trimmedHtml.startsWith(text.trim())) {\n return;\n }\n\n // Generate the embed HTML\n const embedHtml = generateEmbedHtml(parsed, config.autoConvertOptions || {});\n\n // Replace the paragraph content with the embed\n // Use a slight delay to let the input event complete\n setTimeout(() => {\n // Re-check that the node still contains the URL (user might have typed more)\n const currentText = node.textContent || '';\n const currentParsed = parseMediaCMSUrl(currentText);\n\n if (currentParsed && currentParsed.originalUrl === parsed.originalUrl) {\n // Select and replace the entire node\n editor.selection.select(node);\n editor.insertContent(embedHtml);\n }\n }, 100);\n};\n\n/**\n * Check if a text is a MediaCMS URL (public helper).\n *\n * @param {string} text - The text to check\n * @returns {boolean} - True if it's a MediaCMS URL\n */\nexport const isMediaCMSUrl = (text) => {\n return parseMediaCMSUrl(text) !== null;\n};\n\n/**\n * Convert a MediaCMS URL to embed HTML (public helper).\n *\n * @param {string} url - The MediaCMS URL\n * @param {Object} options - Embed options\n * @returns {string|null} - The embed HTML or null if not a valid URL\n */\nexport const convertToEmbed = (url, options = {}) => {\n const parsed = parseMediaCMSUrl(url);\n if (!parsed) {\n return null;\n }\n return generateEmbedHtml(parsed, options);\n};\n"],"names":["MEDIACMS_VIEW_URL_PATTERN","parseMediaCMSUrl","text","trimmed","trim","match","baseUrl","videoId","originalUrl","isDomainAllowed","parsed","config","configuredBaseUrl","autoConvertBaseUrl","mediacmsBaseUrl","configuredUrl","URL","pastedUrl","host","e","generateEmbedHtml","options","embedUrl","searchParams","set","showTitle","showRelated","showUserAvatar","linkTitle","html","toString","editor","autoConvertEnabled","on","handlePasteEvent","handleInputEvent","clipboardData","window","getData","preventDefault","stopPropagation","embedHtml","autoConvertOptions","setTimeout","insertContent","selection","collapse","inputType","node","getNode","nodeName","textContent","trimmedHtml","innerHTML","startsWith","currentText","currentParsed","select","url"],"mappings":";;;;;;;;;;;;MAoCMA,0BAA4B,kDAQ5BC,iBAAoBC,WACjBA,MAAwB,iBAATA,YACT,WAGLC,QAAUD,KAAKE,OAGfC,MAAQF,QAAQE,MAAML,kCACxBK,MACO,CACHC,QAASD,MAAM,GACfE,QAASF,MAAM,GACfG,YAAaL,SAId,MAULM,gBAAkB,CAACC,OAAQC,gBAEvBC,kBAAoBD,OAAOE,oBAAsBF,OAAOG,oBACzDF,yBACM,YAKDG,cAAgB,IAAIC,IAAIJ,mBACxBK,UAAY,IAAID,IAAIN,OAAOJ,gBAC1BS,cAAcG,OAASD,UAAUC,KAC1C,MAAOC,UAEE,IAWTC,kBAAoB,SAACV,YAAQW,+DAAU,SAEnCC,SAAW,IAAIN,cAAON,OAAOJ,mBACnCgB,SAASC,aAAaC,IAAI,IAAKd,OAAOH,SAGtCe,SAASC,aAAaC,IAAI,aAAmC,IAAtBH,QAAQI,UAAsB,IAAM,KAC3EH,SAASC,aAAaC,IAAI,eAAuC,IAAxBH,QAAQK,YAAwB,IAAM,KAC/EJ,SAASC,aAAaC,IAAI,kBAA6C,IAA3BH,QAAQM,eAA2B,IAAM,KACrFL,SAASC,aAAaC,IAAI,aAAmC,IAAtBH,QAAQO,UAAsB,IAAM,WAGrEC,KAAO,sFAGDP,SAASQ,iBAHR,qDAOND,gCASsBE,eACvBpB,QAAS,oBAAQoB,SAAW,IAGA,IAA9BpB,OAAOqB,qBAKXD,OAAOE,GAAG,SAAUd,IAChBe,iBAAiBH,OAAQZ,EAAGR,WAIhCoB,OAAOE,GAAG,SAAUd,IAChBgB,iBAAiBJ,OAAQZ,EAAGR,mBAW9BuB,iBAAmB,CAACH,OAAQZ,EAAGR,gBAE3ByB,cAAgBjB,EAAEiB,eAAiBC,OAAOD,kBAC3CA,2BAKClC,KAAOkC,cAAcE,QAAQ,eAAiBF,cAAcE,QAAQ,YACrEpC,kBAKCQ,OAAST,iBAAiBC,UAC3BQ,kBAKAD,gBAAgBC,OAAQC,eAK7BQ,EAAEoB,iBACFpB,EAAEqB,wBAGIC,UAAYrB,kBAAkBV,OAAQC,OAAO+B,oBAAsB,IAGzEC,YAAW,KACPZ,OAAOa,cAAcH,WAErBV,OAAOc,UAAUC,UAAS,KAC3B,IAWDX,iBAAmB,CAACJ,OAAQZ,EAAGR,aAEb,oBAAhBQ,EAAE4B,WAAmD,eAAhB5B,EAAE4B,uBAKrCC,KAAOjB,OAAOc,UAAUI,cACzBD,MAA0B,MAAlBA,KAAKE,sBAKZhD,KAAO8C,KAAKG,aAAe,GAC3BzC,OAAST,iBAAiBC,UAE3BQ,SAAWD,gBAAgBC,OAAQC,qBAKlCyC,YAAcJ,KAAKK,UAAUjD,UAC/BgD,cAAgBlD,KAAKE,SAAWgD,YAAYE,WAAWpD,KAAKE,qBAK1DqC,UAAYrB,kBAAkBV,OAAQC,OAAO+B,oBAAsB,IAIzEC,YAAW,WAEDY,YAAcP,KAAKG,aAAe,GAClCK,cAAgBvD,iBAAiBsD,aAEnCC,eAAiBA,cAAchD,cAAgBE,OAAOF,cAEtDuB,OAAOc,UAAUY,OAAOT,MACxBjB,OAAOa,cAAcH,cAE1B,6BASuBvC,MACQ,OAA3BD,iBAAiBC,8BAUE,SAACwD,SAAKrC,+DAAU,SACpCX,OAAST,iBAAiByD,YAC3BhD,OAGEU,kBAAkBV,OAAQW,SAFtB"} \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/commands.min.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/commands.min.js new file mode 100755 index 00000000..3835b5e0 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/commands.min.js @@ -0,0 +1,10 @@ +define("tiny_mediacms/commands",["exports","core/str","./common","./iframeembed","editor_tiny/utils"],(function(_exports,_str,_common,_iframeembed,_utils){var obj; +/** + * Tiny Media commands. + * + * @module tiny_mediacms/commands + * @copyright 2022 Huong Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getSetup=void 0,_iframeembed=(obj=_iframeembed)&&obj.__esModule?obj:{default:obj};const isIframe=node=>"iframe"===node.nodeName.toLowerCase()||node.classList&&node.classList.contains("tiny-iframe-responsive")||node.classList&&node.classList.contains("tiny-mediacms-iframe-wrapper"),setupIframeOverlays=(editor,handleIframeAction)=>{const processIframes=()=>{const editorBody=editor.getBody();if(!editorBody)return;editorBody.querySelectorAll("iframe").forEach((iframe=>{var _iframe$parentElement;if(null!==(_iframe$parentElement=iframe.parentElement)&&void 0!==_iframe$parentElement&&_iframe$parentElement.classList.contains("tiny-mediacms-iframe-wrapper"))return;if(iframe.hasAttribute("data-mce-object")||iframe.hasAttribute("data-mce-placeholder"))return;const wrapper=editor.getDoc().createElement("div");wrapper.className="tiny-mediacms-iframe-wrapper",wrapper.setAttribute("contenteditable","false");const editBtn=editor.getDoc().createElement("button");editBtn.className="tiny-mediacms-edit-btn",editBtn.setAttribute("type","button"),editBtn.setAttribute("title","Edit video embed options"),editBtn.innerHTML='',iframe.parentNode.insertBefore(wrapper,iframe),wrapper.appendChild(iframe),wrapper.appendChild(editBtn)}))},handleOverlayClick=e=>{const editBtn=e.target.closest(".tiny-mediacms-edit-btn");if(!editBtn)return;e.preventDefault(),e.stopPropagation();const wrapper=editBtn.closest(".tiny-mediacms-iframe-wrapper");if(!wrapper)return;wrapper.querySelector("iframe")&&(editor.selection.select(wrapper),handleIframeAction())};editor.on("init",(()=>{(()=>{const editorDoc=editor.getDoc();if(!editorDoc)return;if(editorDoc.getElementById("tiny-mediacms-overlay-styles"))return;const style=editorDoc.createElement("style");style.id="tiny-mediacms-overlay-styles",style.textContent="\n .tiny-mediacms-iframe-wrapper {\n display: inline-block;\n position: relative;\n line-height: 0;\n vertical-align: top;\n }\n .tiny-mediacms-iframe-wrapper iframe {\n display: block;\n }\n .tiny-mediacms-edit-btn {\n position: absolute;\n top: 48px;\n left: 6px;\n width: 28px;\n height: 28px;\n background: #ffffff;\n border: none;\n border-radius: 50%;\n cursor: pointer;\n z-index: 10;\n padding: 0;\n margin: 0;\n box-shadow: 0 2px 6px rgba(0,0,0,0.35);\n transition: transform 0.15s, box-shadow 0.15s;\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n }\n .tiny-mediacms-edit-btn:hover {\n transform: scale(1.15);\n box-shadow: 0 3px 10px rgba(0,0,0,0.45);\n }\n .tiny-mediacms-edit-btn svg {\n width: 18px !important;\n height: 18px !important;\n display: block !important;\n }\n ",editorDoc.head.appendChild(style)})(),processIframes(),editor.getBody().addEventListener("click",handleOverlayClick)})),editor.on("SetContent",(()=>{processIframes()})),editor.on("PastePostProcess",(()=>{setTimeout(processIframes,100)})),editor.on("Undo Redo",(()=>{processIframes()})),editor.on("Change",(()=>{setTimeout(processIframes,50)})),editor.on("NodeChange",(()=>{processIframes()}))};_exports.getSetup=async()=>{const[iframeButtonText]=await(0,_str.getStrings)(["iframebuttontitle"].map((key=>({key:key,component:_common.component})))),[iframeButtonImage]=await Promise.all([(0,_utils.getButtonImage)("icon",_common.component)]);return editor=>{((editor,iframeButtonText,iframeButtonImage)=>{const handleIframeAction=()=>{new _iframeembed.default(editor).displayDialogue()};editor.ui.registry.addIcon(_common.iframeIcon,iframeButtonImage.html),editor.ui.registry.addToggleButton(_common.iframeButtonName,{icon:_common.iframeIcon,tooltip:iframeButtonText,onAction:handleIframeAction,onSetup:api=>editor.selection.selectorChangedWithUnbind("iframe:not([data-mce-object]):not([data-mce-placeholder]),.tiny-iframe-responsive,.tiny-mediacms-iframe-wrapper",api.setActive).unbind}),editor.ui.registry.addMenuItem(_common.iframeMenuItemName,{icon:_common.iframeIcon,text:iframeButtonText,onAction:handleIframeAction}),editor.ui.registry.addContextToolbar(_common.iframeButtonName,{predicate:isIframe,items:_common.iframeButtonName,position:"node",scope:"node"}),editor.ui.registry.addContextMenu(_common.iframeButtonName,{update:isIframe}),setupIframeOverlays(editor,handleIframeAction)})(editor,iframeButtonText,iframeButtonImage)}}})); + +//# sourceMappingURL=commands.min.js.map \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/commands.min.js.map b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/commands.min.js.map new file mode 100755 index 00000000..c250916c --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/commands.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.min.js","sources":["../src/commands.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 .\n\n/**\n * Tiny Media commands.\n *\n * @module tiny_mediacms/commands\n * @copyright 2022 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getStrings} from 'core/str';\nimport {\n component,\n iframeButtonName,\n iframeMenuItemName,\n iframeIcon,\n} from './common';\nimport IframeEmbed from './iframeembed';\nimport {getButtonImage} from 'editor_tiny/utils';\n\nconst isIframe = (node) => node.nodeName.toLowerCase() === 'iframe' ||\n (node.classList && node.classList.contains('tiny-iframe-responsive')) ||\n (node.classList && node.classList.contains('tiny-mediacms-iframe-wrapper'));\n\n/**\n * Wrap iframes with overlay containers that allow hover detection.\n * Since iframes capture mouse events, we add an invisible overlay on top\n * that shows the edit button on hover.\n *\n * @param {TinyMCE} editor - The editor instance\n * @param {Function} handleIframeAction - The action to perform when clicking the button\n */\nconst setupIframeOverlays = (editor, handleIframeAction) => {\n /**\n * Process all iframes in the editor and add overlay wrappers.\n */\n const processIframes = () => {\n const editorBody = editor.getBody();\n if (!editorBody) {\n return;\n }\n\n const iframes = editorBody.querySelectorAll('iframe');\n iframes.forEach((iframe) => {\n // Skip if already wrapped\n if (iframe.parentElement?.classList.contains('tiny-mediacms-iframe-wrapper')) {\n return;\n }\n\n // Skip TinyMCE internal iframes\n if (iframe.hasAttribute('data-mce-object') || iframe.hasAttribute('data-mce-placeholder')) {\n return;\n }\n\n // Create wrapper div\n const wrapper = editor.getDoc().createElement('div');\n wrapper.className = 'tiny-mediacms-iframe-wrapper';\n wrapper.setAttribute('contenteditable', 'false');\n\n // Create edit button (positioned inside wrapper, over the iframe)\n const editBtn = editor.getDoc().createElement('button');\n editBtn.className = 'tiny-mediacms-edit-btn';\n editBtn.setAttribute('type', 'button');\n editBtn.setAttribute('title', 'Edit video embed options');\n // Use clean inline SVG to avoid TinyMCE wrapper issues\n editBtn.innerHTML = '' +\n '' +\n '' +\n '';\n\n // Wrap the iframe: insert wrapper, move iframe into it, add button\n iframe.parentNode.insertBefore(wrapper, iframe);\n wrapper.appendChild(iframe);\n wrapper.appendChild(editBtn);\n });\n };\n\n /**\n * Add CSS styles for hover effects to the editor's document.\n */\n const addStyles = () => {\n const editorDoc = editor.getDoc();\n if (!editorDoc) {\n return;\n }\n\n // Check if styles already added\n if (editorDoc.getElementById('tiny-mediacms-overlay-styles')) {\n return;\n }\n\n const style = editorDoc.createElement('style');\n style.id = 'tiny-mediacms-overlay-styles';\n style.textContent = `\n .tiny-mediacms-iframe-wrapper {\n display: inline-block;\n position: relative;\n line-height: 0;\n vertical-align: top;\n }\n .tiny-mediacms-iframe-wrapper iframe {\n display: block;\n }\n .tiny-mediacms-edit-btn {\n position: absolute;\n top: 48px;\n left: 6px;\n width: 28px;\n height: 28px;\n background: #ffffff;\n border: none;\n border-radius: 50%;\n cursor: pointer;\n z-index: 10;\n padding: 0;\n margin: 0;\n box-shadow: 0 2px 6px rgba(0,0,0,0.35);\n transition: transform 0.15s, box-shadow 0.15s;\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n }\n .tiny-mediacms-edit-btn:hover {\n transform: scale(1.15);\n box-shadow: 0 3px 10px rgba(0,0,0,0.45);\n }\n .tiny-mediacms-edit-btn svg {\n width: 18px !important;\n height: 18px !important;\n display: block !important;\n }\n `;\n editorDoc.head.appendChild(style);\n };\n\n /**\n * Handle click on the edit button.\n *\n * @param {Event} e - The click event\n */\n const handleOverlayClick = (e) => {\n const target = e.target;\n\n // Check if clicked on edit button or its child (svg/path)\n const editBtn = target.closest('.tiny-mediacms-edit-btn');\n if (!editBtn) {\n return;\n }\n\n e.preventDefault();\n e.stopPropagation();\n\n // Find the associated wrapper and iframe\n const wrapper = editBtn.closest('.tiny-mediacms-iframe-wrapper');\n if (!wrapper) {\n return;\n }\n\n const iframe = wrapper.querySelector('iframe');\n if (!iframe) {\n return;\n }\n\n // Select the wrapper so TinyMCE knows which element is selected\n editor.selection.select(wrapper);\n\n // Open the edit dialog\n handleIframeAction();\n };\n\n // Setup on editor init\n editor.on('init', () => {\n addStyles();\n processIframes();\n\n // Handle clicks on the overlay\n editor.getBody().addEventListener('click', handleOverlayClick);\n });\n\n // Re-process when content changes\n editor.on('SetContent', () => {\n processIframes();\n });\n\n // Re-process when content is pasted\n editor.on('PastePostProcess', () => {\n setTimeout(processIframes, 100);\n });\n\n // Re-process after undo/redo\n editor.on('Undo Redo', () => {\n processIframes();\n });\n\n // Re-process on any content change (covers modal updates)\n editor.on('Change', () => {\n setTimeout(processIframes, 50);\n });\n\n // Re-process when node changes (selection changes)\n editor.on('NodeChange', () => {\n processIframes();\n });\n};\n\nconst registerIframeCommand = (editor, iframeButtonText, iframeButtonImage) => {\n const handleIframeAction = () => {\n const iframeEmbed = new IframeEmbed(editor);\n iframeEmbed.displayDialogue();\n };\n\n // Register the iframe icon\n editor.ui.registry.addIcon(iframeIcon, iframeButtonImage.html);\n\n // Register the Menu Button as a toggle.\n // This means that when highlighted over an existing iframe element it will show as toggled on.\n editor.ui.registry.addToggleButton(iframeButtonName, {\n icon: iframeIcon,\n tooltip: iframeButtonText,\n onAction: handleIframeAction,\n onSetup: api => {\n return editor.selection.selectorChangedWithUnbind(\n 'iframe:not([data-mce-object]):not([data-mce-placeholder]),.tiny-iframe-responsive,.tiny-mediacms-iframe-wrapper',\n api.setActive\n ).unbind;\n }\n });\n\n editor.ui.registry.addMenuItem(iframeMenuItemName, {\n icon: iframeIcon,\n text: iframeButtonText,\n onAction: handleIframeAction,\n });\n\n editor.ui.registry.addContextToolbar(iframeButtonName, {\n predicate: isIframe,\n items: iframeButtonName,\n position: 'node',\n scope: 'node'\n });\n\n editor.ui.registry.addContextMenu(iframeButtonName, {\n update: isIframe,\n });\n\n // Setup iframe overlays with edit button on hover\n setupIframeOverlays(editor, handleIframeAction);\n};\n\nexport const getSetup = async() => {\n const [\n iframeButtonText,\n ] = await getStrings([\n 'iframebuttontitle',\n ].map((key) => ({key, component})));\n\n const [\n iframeButtonImage,\n ] = await Promise.all([\n getButtonImage('icon', component),\n ]);\n\n // Note: The function returned here must be synchronous and cannot use promises.\n // All promises must be resolved prior to returning the function.\n return (editor) => {\n registerIframeCommand(editor, iframeButtonText, iframeButtonImage);\n };\n};\n"],"names":["isIframe","node","nodeName","toLowerCase","classList","contains","setupIframeOverlays","editor","handleIframeAction","processIframes","editorBody","getBody","querySelectorAll","forEach","iframe","parentElement","_iframe$parentElement","hasAttribute","wrapper","getDoc","createElement","className","setAttribute","editBtn","innerHTML","parentNode","insertBefore","appendChild","handleOverlayClick","e","target","closest","preventDefault","stopPropagation","querySelector","selection","select","on","editorDoc","getElementById","style","id","textContent","head","addStyles","addEventListener","setTimeout","async","iframeButtonText","map","key","component","iframeButtonImage","Promise","all","IframeEmbed","displayDialogue","ui","registry","addIcon","iframeIcon","html","addToggleButton","iframeButtonName","icon","tooltip","onAction","onSetup","api","selectorChangedWithUnbind","setActive","unbind","addMenuItem","iframeMenuItemName","text","addContextToolbar","predicate","items","position","scope","addContextMenu","update","registerIframeCommand"],"mappings":";;;;;;;8JAiCMA,SAAYC,MAAyC,WAAhCA,KAAKC,SAASC,eACpCF,KAAKG,WAAaH,KAAKG,UAAUC,SAAS,2BAC1CJ,KAAKG,WAAaH,KAAKG,UAAUC,SAAS,gCAUzCC,oBAAsB,CAACC,OAAQC,4BAI3BC,eAAiB,WACbC,WAAaH,OAAOI,cACrBD,kBAIWA,WAAWE,iBAAiB,UACpCC,SAASC,oEAETA,OAAOC,gDAAPC,sBAAsBZ,UAAUC,SAAS,0CAKzCS,OAAOG,aAAa,oBAAsBH,OAAOG,aAAa,qCAK5DC,QAAUX,OAAOY,SAASC,cAAc,OAC9CF,QAAQG,UAAY,+BACpBH,QAAQI,aAAa,kBAAmB,eAGlCC,QAAUhB,OAAOY,SAASC,cAAc,UAC9CG,QAAQF,UAAY,yBACpBE,QAAQD,aAAa,OAAQ,UAC7BC,QAAQD,aAAa,QAAS,4BAE9BC,QAAQC,UAAY,0KAMpBV,OAAOW,WAAWC,aAAaR,QAASJ,QACxCI,QAAQS,YAAYb,QACpBI,QAAQS,YAAYJ,aAoEtBK,mBAAsBC,UAIlBN,QAHSM,EAAEC,OAGMC,QAAQ,+BAC1BR,eAILM,EAAEG,iBACFH,EAAEI,wBAGIf,QAAUK,QAAQQ,QAAQ,qCAC3Bb,eAIUA,QAAQgB,cAAc,YAMrC3B,OAAO4B,UAAUC,OAAOlB,SAGxBV,uBAIJD,OAAO8B,GAAG,QAAQ,KA5FA,YACRC,UAAY/B,OAAOY,aACpBmB,oBAKDA,UAAUC,eAAe,6CAIvBC,MAAQF,UAAUlB,cAAc,SACtCoB,MAAMC,GAAK,+BACXD,MAAME,02CAwCNJ,UAAUK,KAAKhB,YAAYa,QAwC3BI,GACAnC,iBAGAF,OAAOI,UAAUkC,iBAAiB,QAASjB,uBAI/CrB,OAAO8B,GAAG,cAAc,KACpB5B,oBAIJF,OAAO8B,GAAG,oBAAoB,KAC1BS,WAAWrC,eAAgB,QAI/BF,OAAO8B,GAAG,aAAa,KACnB5B,oBAIJF,OAAO8B,GAAG,UAAU,KAChBS,WAAWrC,eAAgB,OAI/BF,OAAO8B,GAAG,cAAc,KACpB5B,uCAgDgBsC,gBAEhBC,wBACM,mBAAW,CACjB,qBACFC,KAAKC,OAAUA,IAAAA,IAAKC,UAAAA,wBAGlBC,yBACMC,QAAQC,IAAI,EAClB,yBAAe,OAAQH,4BAKnB5C,SA3DkB,EAACA,OAAQyC,iBAAkBI,2BAC/C5C,mBAAqB,KACH,IAAI+C,qBAAYhD,QACxBiD,mBAIhBjD,OAAOkD,GAAGC,SAASC,QAAQC,mBAAYR,kBAAkBS,MAIzDtD,OAAOkD,GAAGC,SAASI,gBAAgBC,yBAAkB,CACjDC,KAAMJ,mBACNK,QAASjB,iBACTkB,SAAU1D,mBACV2D,QAASC,KACE7D,OAAO4B,UAAUkC,0BACpB,kHACAD,IAAIE,WACNC,SAIVhE,OAAOkD,GAAGC,SAASc,YAAYC,2BAAoB,CAC/CT,KAAMJ,mBACNc,KAAM1B,iBACNkB,SAAU1D,qBAGdD,OAAOkD,GAAGC,SAASiB,kBAAkBZ,yBAAkB,CACnDa,UAAW5E,SACX6E,MAAOd,yBACPe,SAAU,OACVC,MAAO,SAGXxE,OAAOkD,GAAGC,SAASsB,eAAejB,yBAAkB,CAChDkB,OAAQjF,WAIZM,oBAAoBC,OAAQC,qBAmBxB0E,CAAsB3E,OAAQyC,iBAAkBI"} \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/common.min.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/common.min.js new file mode 100755 index 00000000..7daa212e --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/common.min.js @@ -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 \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/common.min.js.map b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/common.min.js.map new file mode 100755 index 00000000..5d06ebcf --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/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 .\n\n/**\n * Tiny Media common values.\n *\n * @module tiny_mediacms/common\n * @copyright 2022 Huong Nguyen \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"} \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/configuration.min.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/configuration.min.js new file mode 100755 index 00000000..c2265e1a --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/configuration.min.js @@ -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 \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/configuration.min.js.map b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/configuration.min.js.map new file mode 100755 index 00000000..635c33a4 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/configuration.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"configuration.min.js","sources":["../src/configuration.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Tiny Media configuration.\n *\n * @module tiny_mediacms/configuration\n * @copyright 2022 Huong Nguyen \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"} \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/embed.min.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/embed.min.js new file mode 100755 index 00000000..5a81b3b7 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/embed.min.js @@ -0,0 +1,3 @@ +define("tiny_mediacms/embed",["exports","core/templates","core/str","core/modal_events","editor_tiny/utils","editor_tiny/options","./common","./embedmodal","./selectors","./options"],(function(_exports,_templates,_str,ModalEvents,_utils,_options,_common,_embedmodal,_selectors,_options2){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),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),_embedmodal=_interopRequireDefault(_embedmodal),_selectors=_interopRequireDefault(_selectors);return _exports.default=class{constructor(editor){_defineProperty(this,"editor",null),_defineProperty(this,"canShowFilePicker",!1),_defineProperty(this,"canShowFilePickerPoster",!1),_defineProperty(this,"canShowFilePickerTrack",!1),_defineProperty(this,"helpStrings",null),_defineProperty(this,"isUpdating",!1),_defineProperty(this,"selectedMedia",null);const permissions=(0,_options2.getEmbedPermissions)(editor);this.canShowFilePicker=permissions.filepicker&&void 0!==(0,_options.getFilePicker)(editor,"media"),this.canShowFilePickerPoster=permissions.filepicker&&void 0!==(0,_options.getFilePicker)(editor,"image"),this.canShowFilePickerTrack=permissions.filepicker&&void 0!==(0,_options.getFilePicker)(editor,"subtitle"),this.editor=editor}async getHelpStrings(){if(!this.helpStrings){const[addSource,tracks,subtitles,captions,descriptions,chapters,metadata]=await(0,_str.getStrings)(["addsource_help","tracks_help","subtitles_help","captions_help","descriptions_help","chapters_help","metadata_help"].map((key=>({key:key,component:_common.component}))));this.helpStrings={addSource:addSource,tracks:tracks,subtitles:subtitles,captions:captions,descriptions:descriptions,chapters:chapters,metadata:metadata}}return this.helpStrings}async getTemplateContext(data){const languages=this.prepareMoodleLang(),helpIcons=Array.from(Object.entries(await this.getHelpStrings())).forEach((_ref=>{let[key,text]=_ref;data["".concat(key.toLowerCase(),"helpicon")]={text: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:!0,video:!1,audio:!1,isupdating:this.isUpdating},data,helpIcons)}async displayDialogue(){this.selectedMedia=this.getSelectedMedia();const data=Object.assign({},this.getCurrentEmbedData());this.isUpdating=0!==Object.keys(data).length,this.currentModal=await _embedmodal.default.create({title:(0,_str.getString)("createmedia","tiny_mediacms"),templateContext:await this.getTemplateContext(data)}),await this.registerEventListeners(this.currentModal)}getCurrentEmbedData(){const properties=this.getMediumProperties();if(!properties)return{};const processedProperties={};return processedProperties[properties.type.toLowerCase()]=properties,processedProperties.link=!1,processedProperties}getSelectedMedia(){const mediaElm=this.editor.selection.getNode();return mediaElm?"video"===mediaElm.nodeName.toLowerCase()||"audio"===mediaElm.nodeName.toLowerCase()?mediaElm:mediaElm.querySelector("video")?mediaElm.querySelector("video"):mediaElm.querySelector("audio")?mediaElm.querySelector("audio"):null:null}getMediumProperties(){const boolAttr=(elem,attr)=>elem.hasAttribute(attr)&&(elem.getAttribute(attr)||""===elem.getAttribute(attr)),tracks={subtitles:[],captions:[],descriptions:[],chapters:[],metadata:[]},sources=[],medium=this.selectedMedia;return medium?(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)})),{type:"video"===medium.nodeName.toLowerCase()?_selectors.default.EMBED.mediaTypes.video:_selectors.default.EMBED.mediaTypes.audio,sources: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:tracks}):null}prepareMoodleLang(){const moodleLangs=(0,_options.getMoodleLang)(this.editor),currentLanguage=(0,_options.getCurrentLanguage)(this.editor);return{installed:Object.entries(moodleLangs.installed).map((_ref2=>{let[lang,code]=_ref2;return{lang:lang,code:code,default:lang===currentLanguage}})),available:Object.entries(moodleLangs.available).map((_ref3=>{let[lang,code]=_ref3;return{lang:lang,code:code,default:lang===currentLanguage}}))}}getMoodleLangObj(subtitleLang){const{available:available}=(0,_options.getMoodleLang)(this.editor);return available[subtitleLang]?{lang:subtitleLang,code:available[subtitleLang]}:null}filePickerCallback(params,element,fpType){if(""!==params.url){const tabPane=element.closest(".tab-pane");if(element.closest(_selectors.default.EMBED.elements.source).querySelector(_selectors.default.EMBED.elements.url).value=params.url,tabPane.id===this.editor.getElement().id+"_"+_selectors.default.EMBED.mediaTypes.link.toLowerCase()&&(tabPane.querySelector(_selectors.default.EMBED.elements.name).value=params.file),"subtitle"===fpType){const subtitleLang=params.file.split(".vtt")[0].split("-").slice(-1)[0],langObj=this.getMoodleLangObj(subtitleLang);if(langObj){const track=element.closest(_selectors.default.EMBED.elements.track);track.querySelector(_selectors.default.EMBED.elements.trackLabel).value=langObj.lang.trim(),track.querySelector(_selectors.default.EMBED.elements.trackLang).value=langObj.code}}}}addMediaSourceComponent(element,callback){const sourceElement=element.closest(_selectors.default.EMBED.elements.source+_selectors.default.EMBED.elements.mediaSource),clone=sourceElement.cloneNode(!0);sourceElement.querySelector(".removecomponent-wrapper").classList.remove("hidden"),sourceElement.querySelector(".addcomponent-wrapper").classList.add("hidden"),sourceElement.parentNode.insertBefore(clone,sourceElement.nextSibling),callback&&callback(clone)}removeMediaSourceComponent(element){element.closest(_selectors.default.EMBED.elements.source+_selectors.default.EMBED.elements.mediaSource).remove()}addTrackComponent(element,callback){const trackElement=element.closest(_selectors.default.EMBED.elements.track),clone=trackElement.cloneNode(!0);trackElement.querySelector(".removecomponent-wrapper").classList.remove("hidden"),trackElement.querySelector(".addcomponent-wrapper").classList.add("hidden"),trackElement.parentNode.insertBefore(clone,trackElement.nextSibling),callback&&callback(clone)}removeTrackComponent(element){element.closest(_selectors.default.EMBED.elements.track).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")),tabContent=form.querySelector(_selectors.default.EMBED.elements[mediumType.toLowerCase()+"Pane"]);return this["getMediaHTML"+mediumType[0].toUpperCase()+mediumType.substr(1)](tabContent)}getMediaHTMLLink(tab){const context={url:tab.querySelector(_selectors.default.EMBED.elements.url).value,name:tab.querySelector(_selectors.default.EMBED.elements.name).value||!1};return context.url?_templates.default.renderForPromise("tiny_mediacms/embed_media_link",context):""}getMediaHTMLVideo(tab){const context=this.getContextForMediaHTML(tab);return context.width=tab.querySelector(_selectors.default.EMBED.elements.width).value||!1,context.height=tab.querySelector(_selectors.default.EMBED.elements.height).value||!1,context.poster=tab.querySelector("".concat(_selectors.default.EMBED.elements.posterSource," ").concat(_selectors.default.EMBED.elements.url)).value||!1,context.sources.length?_templates.default.renderForPromise("tiny_mediacms/embed_media_video",context):""}getMediaHTMLAudio(tab){const context=this.getContextForMediaHTML(tab);return context.sources.length?_templates.default.renderForPromise("tiny_mediacms/embed_media_audio",context):""}getContextForMediaHTML(tab){const tracks=Array.from(tab.querySelectorAll(_selectors.default.EMBED.elements.track)).map((track=>({track:track.querySelector(_selectors.default.EMBED.elements.trackSource+" "+_selectors.default.EMBED.elements.url).value,kind:this.getTrackTypeFromTabPane(track.closest(".tab-pane")),label:track.querySelector(_selectors.default.EMBED.elements.trackLabel).value||track.querySelector(_selectors.default.EMBED.elements.trackLang).value,srclang:track.querySelector(_selectors.default.EMBED.elements.trackLang).value,defaultTrack:track.querySelector(_selectors.default.EMBED.elements.trackDefault).checked?"true":null}))).filter((track=>!!track.track));return{sources:Array.from(tab.querySelectorAll(_selectors.default.EMBED.elements.mediaSource+" "+_selectors.default.EMBED.elements.url)).filter((source=>!!source.value)).map((source=>source.value)),description:tab.querySelector(_selectors.default.EMBED.elements.mediaSource+" "+_selectors.default.EMBED.elements.url).value||!1,tracks:tracks,showControls:tab.querySelector(_selectors.default.EMBED.elements.mediaControl).checked,autoplay:tab.querySelector(_selectors.default.EMBED.elements.mediaAutoplay).checked,muted:tab.querySelector(_selectors.default.EMBED.elements.mediaMute).checked,loop:tab.querySelector(_selectors.default.EMBED.elements.mediaLoop).checked,title:tab.querySelector(_selectors.default.EMBED.elements.title).value||!1}}getFilepickerTypeFromElement(element){return element.closest(_selectors.default.EMBED.elements.posterSource)?"image":element.closest(_selectors.default.EMBED.elements.trackSource)?"subtitle":"media"}async clickHandler(e){const element=e.target;if(element.closest(_selectors.default.EMBED.actions.mediaBrowser)){e.preventDefault();const fpType=this.getFilepickerTypeFromElement(element),params=await(0,_utils.displayFilepicker)(this.editor,fpType);this.filePickerCallback(params,element,fpType)}element.closest(_selectors.default.EMBED.elements.mediaSource+" .addcomponent")&&(e.preventDefault(),this.addMediaSourceComponent(element));element.closest(_selectors.default.EMBED.elements.mediaSource+" .removecomponent")&&(e.preventDefault(),this.removeMediaSourceComponent(element));element.closest(_selectors.default.EMBED.elements.track+" .addcomponent")&&(e.preventDefault(),this.addTrackComponent(element));element.closest(_selectors.default.EMBED.elements.track+" .removecomponent")&&(e.preventDefault(),this.removeTrackComponent(element));const trackDefaultAction=element.closest(_selectors.default.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.default.EMBED.elements.trackDefault).forEach((select=>{select!==element&&getKind(element)===getKind(select)&&(select.checked=!1)}))}}async handleDialogueSubmission(event,modal){const{html:html}=await this.getMediaHTML(modal.getRoot()[0]);html&&(this.isUpdating?(this.selectedMedia.outerHTML=html,this.isUpdating=!1):this.editor.insertContent(html))}async registerEventListeners(modal){await modal.getBody();const $root=modal.getRoot(),root=$root[0];(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.default.EMBED.elements.trackLang).forEach((dropdown=>{const defaultVal=dropdown.getAttribute("data-value");defaultVal&&(dropdown.value=defaultVal)}))}))}},_exports.default})); + +//# sourceMappingURL=embed.min.js.map \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/embed.min.js.map b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/embed.min.js.map new file mode 100755 index 00000000..ea4585b0 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/embed.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"embed.min.js","sources":["../src/embed.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 .\n\n/**\n * Tiny Media plugin Embed class for Moodle.\n *\n * @module tiny_mediacms/embed\n * @copyright 2022 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport {\n getString,\n getStrings,\n} from 'core/str';\nimport * as ModalEvents from 'core/modal_events';\nimport {displayFilepicker} from 'editor_tiny/utils';\nimport {getCurrentLanguage, getMoodleLang} from 'editor_tiny/options';\nimport {component} from \"./common\";\nimport EmbedModal from './embedmodal';\nimport Selectors from './selectors';\nimport {getEmbedPermissions} from './options';\nimport {getFilePicker} from 'editor_tiny/options';\n\nexport default class MediaEmbed {\n editor = null;\n canShowFilePicker = false;\n canShowFilePickerPoster = false;\n canShowFilePickerTrack = false;\n\n /**\n * @property {Object} The names of the alignment options.\n */\n helpStrings = null;\n\n /**\n * @property {boolean} Indicate that the user is updating the media or not.\n */\n isUpdating = false;\n\n /**\n * @property {Object} The currently selected media.\n */\n selectedMedia = null;\n\n constructor(editor) {\n const permissions = getEmbedPermissions(editor);\n\n // Indicates whether the file picker can be shown.\n this.canShowFilePicker = permissions.filepicker && (typeof getFilePicker(editor, 'media') !== 'undefined');\n this.canShowFilePickerPoster = permissions.filepicker && (typeof getFilePicker(editor, 'image') !== 'undefined');\n this.canShowFilePickerTrack = permissions.filepicker && (typeof getFilePicker(editor, 'subtitle') !== 'undefined');\n\n this.editor = editor;\n }\n\n async getHelpStrings() {\n if (!this.helpStrings) {\n const [addSource, tracks, subtitles, captions, descriptions, chapters, metadata] = await getStrings([\n 'addsource_help',\n 'tracks_help',\n 'subtitles_help',\n 'captions_help',\n 'descriptions_help',\n 'chapters_help',\n 'metadata_help',\n ].map((key) => ({\n key,\n component,\n })));\n\n this.helpStrings = {addSource, tracks, subtitles, captions, descriptions, chapters, metadata};\n }\n\n return this.helpStrings;\n }\n\n async getTemplateContext(data) {\n const languages = this.prepareMoodleLang();\n\n const helpIcons = Array.from(Object.entries(await this.getHelpStrings())).forEach(([key, text]) => {\n data[`${key.toLowerCase()}helpicon`] = {text};\n });\n\n return Object.assign({}, {\n elementid: this.editor.getElement().id,\n showfilepicker: this.canShowFilePicker,\n showfilepickerposter: this.canShowFilePickerPoster,\n showfilepickertrack: this.canShowFilePickerTrack,\n langsinstalled: languages.installed,\n langsavailable: languages.available,\n link: true,\n video: false,\n audio: false,\n isupdating: this.isUpdating,\n }, data, helpIcons);\n }\n\n async displayDialogue() {\n this.selectedMedia = this.getSelectedMedia();\n const data = Object.assign({}, this.getCurrentEmbedData());\n this.isUpdating = Object.keys(data).length !== 0;\n\n this.currentModal = await EmbedModal.create({\n title: getString('createmedia', 'tiny_mediacms'),\n templateContext: await this.getTemplateContext(data),\n });\n\n await this.registerEventListeners(this.currentModal);\n }\n\n getCurrentEmbedData() {\n const properties = this.getMediumProperties();\n if (!properties) {\n return {};\n }\n\n const processedProperties = {};\n processedProperties[properties.type.toLowerCase()] = properties;\n processedProperties.link = false;\n\n return processedProperties;\n }\n\n getSelectedMedia() {\n const mediaElm = this.editor.selection.getNode();\n\n if (!mediaElm) {\n return null;\n }\n\n if (mediaElm.nodeName.toLowerCase() === 'video' || mediaElm.nodeName.toLowerCase() === 'audio') {\n return mediaElm;\n }\n\n if (mediaElm.querySelector('video')) {\n return mediaElm.querySelector('video');\n }\n\n if (mediaElm.querySelector('audio')) {\n return mediaElm.querySelector('audio');\n }\n\n return null;\n }\n\n getMediumProperties() {\n const boolAttr = (elem, attr) => {\n // As explained in MDL-64175, some OS (like Ubuntu), are removing the value for these attributes.\n // So in order to check if attr=\"true\", we need to check if the attribute exists and if the value is empty or true.\n return (elem.hasAttribute(attr) && (elem.getAttribute(attr) || elem.getAttribute(attr) === ''));\n };\n\n const tracks = {\n subtitles: [],\n captions: [],\n descriptions: [],\n chapters: [],\n metadata: []\n };\n const sources = [];\n\n const medium = this.selectedMedia;\n if (!medium) {\n return null;\n }\n medium.querySelectorAll('track').forEach((track) => {\n tracks[track.getAttribute('kind')].push({\n src: track.getAttribute('src'),\n srclang: track.getAttribute('srclang'),\n label: track.getAttribute('label'),\n defaultTrack: boolAttr(track, 'default')\n });\n });\n\n medium.querySelectorAll('source').forEach((source) => {\n sources.push(source.src);\n });\n\n return {\n type: medium.nodeName.toLowerCase() === 'video' ? Selectors.EMBED.mediaTypes.video : Selectors.EMBED.mediaTypes.audio,\n sources,\n poster: medium.getAttribute('poster'),\n title: medium.getAttribute('title'),\n width: medium.getAttribute('width'),\n height: medium.getAttribute('height'),\n autoplay: boolAttr(medium, 'autoplay'),\n loop: boolAttr(medium, 'loop'),\n muted: boolAttr(medium, 'muted'),\n controls: boolAttr(medium, 'controls'),\n tracks,\n };\n }\n\n prepareMoodleLang() {\n const moodleLangs = getMoodleLang(this.editor);\n const currentLanguage = getCurrentLanguage(this.editor);\n\n const installed = Object.entries(moodleLangs.installed).map(([lang, code]) => ({\n lang,\n code,\n \"default\": lang === currentLanguage,\n }));\n\n const available = Object.entries(moodleLangs.available).map(([lang, code]) => ({\n lang,\n code,\n \"default\": lang === currentLanguage,\n }));\n\n return {\n installed,\n available,\n };\n }\n\n getMoodleLangObj(subtitleLang) {\n const {available} = getMoodleLang(this.editor);\n\n if (available[subtitleLang]) {\n return {\n lang: subtitleLang,\n code: available[subtitleLang],\n };\n }\n\n return null;\n }\n\n filePickerCallback(params, element, fpType) {\n if (params.url !== '') {\n const tabPane = element.closest('.tab-pane');\n element.closest(Selectors.EMBED.elements.source).querySelector(Selectors.EMBED.elements.url).value = params.url;\n\n if (tabPane.id === this.editor.getElement().id + '_' + Selectors.EMBED.mediaTypes.link.toLowerCase()) {\n tabPane.querySelector(Selectors.EMBED.elements.name).value = params.file;\n }\n\n if (fpType === 'subtitle') {\n // If the file is subtitle file. We need to match the language and label for that file.\n const subtitleLang = params.file.split('.vtt')[0].split('-').slice(-1)[0];\n const langObj = this.getMoodleLangObj(subtitleLang);\n if (langObj) {\n const track = element.closest(Selectors.EMBED.elements.track);\n track.querySelector(Selectors.EMBED.elements.trackLabel).value = langObj.lang.trim();\n track.querySelector(Selectors.EMBED.elements.trackLang).value = langObj.code;\n }\n }\n }\n }\n\n addMediaSourceComponent(element, callback) {\n const sourceElement = element.closest(Selectors.EMBED.elements.source + Selectors.EMBED.elements.mediaSource);\n const clone = sourceElement.cloneNode(true);\n\n sourceElement.querySelector('.removecomponent-wrapper').classList.remove('hidden');\n sourceElement.querySelector('.addcomponent-wrapper').classList.add('hidden');\n\n sourceElement.parentNode.insertBefore(clone, sourceElement.nextSibling);\n\n if (callback) {\n callback(clone);\n }\n }\n\n removeMediaSourceComponent(element) {\n const sourceElement = element.closest(Selectors.EMBED.elements.source + Selectors.EMBED.elements.mediaSource);\n sourceElement.remove();\n }\n\n addTrackComponent(element, callback) {\n const trackElement = element.closest(Selectors.EMBED.elements.track);\n const clone = trackElement.cloneNode(true);\n\n trackElement.querySelector('.removecomponent-wrapper').classList.remove('hidden');\n trackElement.querySelector('.addcomponent-wrapper').classList.add('hidden');\n\n trackElement.parentNode.insertBefore(clone, trackElement.nextSibling);\n\n if (callback) {\n callback(clone);\n }\n }\n\n removeTrackComponent(element) {\n const sourceElement = element.closest(Selectors.EMBED.elements.track);\n sourceElement.remove();\n }\n\n getMediumTypeFromTabPane(tabPane) {\n return tabPane.getAttribute('data-medium-type');\n }\n\n getTrackTypeFromTabPane(tabPane) {\n return tabPane.getAttribute('data-track-kind');\n }\n\n getMediaHTML(form) {\n const mediumType = this.getMediumTypeFromTabPane(form.querySelector('.root.tab-content > .tab-pane.active'));\n const tabContent = form.querySelector(Selectors.EMBED.elements[mediumType.toLowerCase() + 'Pane']);\n\n return this['getMediaHTML' + mediumType[0].toUpperCase() + mediumType.substr(1)](tabContent);\n }\n\n getMediaHTMLLink(tab) {\n const context = {\n url: tab.querySelector(Selectors.EMBED.elements.url).value,\n name: tab.querySelector(Selectors.EMBED.elements.name).value || false\n };\n\n return context.url ? Templates.renderForPromise('tiny_mediacms/embed_media_link', context) : '';\n }\n\n getMediaHTMLVideo(tab) {\n const context = this.getContextForMediaHTML(tab);\n context.width = tab.querySelector(Selectors.EMBED.elements.width).value || false;\n context.height = tab.querySelector(Selectors.EMBED.elements.height).value || false;\n context.poster = tab.querySelector(\n `${Selectors.EMBED.elements.posterSource} ${Selectors.EMBED.elements.url}`\n ).value || false;\n\n return context.sources.length ? Templates.renderForPromise('tiny_mediacms/embed_media_video', context) : '';\n }\n\n getMediaHTMLAudio(tab) {\n const context = this.getContextForMediaHTML(tab);\n\n return context.sources.length ? Templates.renderForPromise('tiny_mediacms/embed_media_audio', context) : '';\n }\n\n getContextForMediaHTML(tab) {\n const tracks = Array.from(tab.querySelectorAll(Selectors.EMBED.elements.track)).map(track => ({\n track: track.querySelector(Selectors.EMBED.elements.trackSource + ' ' + Selectors.EMBED.elements.url).value,\n kind: this.getTrackTypeFromTabPane(track.closest('.tab-pane')),\n label: track.querySelector(Selectors.EMBED.elements.trackLabel).value ||\n track.querySelector(Selectors.EMBED.elements.trackLang).value,\n srclang: track.querySelector(Selectors.EMBED.elements.trackLang).value,\n defaultTrack: track.querySelector(Selectors.EMBED.elements.trackDefault).checked ? \"true\" : null\n })).filter((track) => !!track.track);\n\n const sources = Array.from(tab.querySelectorAll(Selectors.EMBED.elements.mediaSource + ' '\n + Selectors.EMBED.elements.url))\n .filter((source) => !!source.value)\n .map((source) => source.value);\n\n return {\n sources,\n description: tab.querySelector(Selectors.EMBED.elements.mediaSource + ' '\n + Selectors.EMBED.elements.url).value || false,\n tracks,\n showControls: tab.querySelector(Selectors.EMBED.elements.mediaControl).checked,\n autoplay: tab.querySelector(Selectors.EMBED.elements.mediaAutoplay).checked,\n muted: tab.querySelector(Selectors.EMBED.elements.mediaMute).checked,\n loop: tab.querySelector(Selectors.EMBED.elements.mediaLoop).checked,\n title: tab.querySelector(Selectors.EMBED.elements.title).value || false\n };\n }\n\n getFilepickerTypeFromElement(element) {\n if (element.closest(Selectors.EMBED.elements.posterSource)) {\n return 'image';\n }\n if (element.closest(Selectors.EMBED.elements.trackSource)) {\n return 'subtitle';\n }\n\n return 'media';\n }\n\n async clickHandler(e) {\n const element = e.target;\n\n const mediaBrowser = element.closest(Selectors.EMBED.actions.mediaBrowser);\n if (mediaBrowser) {\n e.preventDefault();\n const fpType = this.getFilepickerTypeFromElement(element);\n const params = await displayFilepicker(this.editor, fpType);\n this.filePickerCallback(params, element, fpType);\n }\n\n const addComponentSourceAction = element.closest(Selectors.EMBED.elements.mediaSource + ' .addcomponent');\n if (addComponentSourceAction) {\n e.preventDefault();\n this.addMediaSourceComponent(element);\n }\n\n const removeComponentSourceAction = element.closest(Selectors.EMBED.elements.mediaSource + ' .removecomponent');\n if (removeComponentSourceAction) {\n e.preventDefault();\n this.removeMediaSourceComponent(element);\n }\n\n const addComponentTrackAction = element.closest(Selectors.EMBED.elements.track + ' .addcomponent');\n if (addComponentTrackAction) {\n e.preventDefault();\n this.addTrackComponent(element);\n }\n\n const removeComponentTrackAction = element.closest(Selectors.EMBED.elements.track + ' .removecomponent');\n if (removeComponentTrackAction) {\n e.preventDefault();\n this.removeTrackComponent(element);\n }\n\n // Only allow one track per tab to be selected as \"default\".\n const trackDefaultAction = element.closest(Selectors.EMBED.elements.trackDefault);\n if (trackDefaultAction && trackDefaultAction.checked) {\n const getKind = (el) => this.getTrackTypeFromTabPane(el.parentElement.closest('.tab-pane'));\n\n element.parentElement\n .closest('.root.tab-content')\n .querySelectorAll(Selectors.EMBED.elements.trackDefault)\n .forEach((select) => {\n if (select !== element && getKind(element) === getKind(select)) {\n select.checked = false;\n }\n });\n }\n }\n\n async handleDialogueSubmission(event, modal) {\n const {html} = await this.getMediaHTML(modal.getRoot()[0]);\n if (html) {\n if (this.isUpdating) {\n this.selectedMedia.outerHTML = html;\n this.isUpdating = false;\n } else {\n this.editor.insertContent(html);\n }\n }\n }\n\n async registerEventListeners(modal) {\n await modal.getBody();\n const $root = modal.getRoot();\n const root = $root[0];\n if (this.canShowFilePicker || this.canShowFilePickerPoster || this.canShowFilePickerTrack) {\n root.addEventListener('click', this.clickHandler.bind(this));\n }\n\n $root.on(ModalEvents.save, this.handleDialogueSubmission.bind(this));\n $root.on(ModalEvents.hidden, () => {\n this.currentModal.destroy();\n });\n $root.on(ModalEvents.shown, () => {\n root.querySelectorAll(Selectors.EMBED.elements.trackLang).forEach((dropdown) => {\n const defaultVal = dropdown.getAttribute('data-value');\n if (defaultVal) {\n dropdown.value = defaultVal;\n }\n });\n });\n }\n}\n"],"names":["constructor","editor","permissions","canShowFilePicker","filepicker","canShowFilePickerPoster","canShowFilePickerTrack","this","helpStrings","addSource","tracks","subtitles","captions","descriptions","chapters","metadata","map","key","component","data","languages","prepareMoodleLang","helpIcons","Array","from","Object","entries","getHelpStrings","forEach","_ref","text","toLowerCase","assign","elementid","getElement","id","showfilepicker","showfilepickerposter","showfilepickertrack","langsinstalled","installed","langsavailable","available","link","video","audio","isupdating","isUpdating","selectedMedia","getSelectedMedia","getCurrentEmbedData","keys","length","currentModal","EmbedModal","create","title","templateContext","getTemplateContext","registerEventListeners","properties","getMediumProperties","processedProperties","type","mediaElm","selection","getNode","nodeName","querySelector","boolAttr","elem","attr","hasAttribute","getAttribute","sources","medium","querySelectorAll","track","push","src","srclang","label","defaultTrack","source","Selectors","EMBED","mediaTypes","poster","width","height","autoplay","loop","muted","controls","moodleLangs","currentLanguage","_ref2","lang","code","_ref3","getMoodleLangObj","subtitleLang","filePickerCallback","params","element","fpType","url","tabPane","closest","elements","value","name","file","split","slice","langObj","trackLabel","trim","trackLang","addMediaSourceComponent","callback","sourceElement","mediaSource","clone","cloneNode","classList","remove","add","parentNode","insertBefore","nextSibling","removeMediaSourceComponent","addTrackComponent","trackElement","removeTrackComponent","getMediumTypeFromTabPane","getTrackTypeFromTabPane","getMediaHTML","form","mediumType","tabContent","toUpperCase","substr","getMediaHTMLLink","tab","context","Templates","renderForPromise","getMediaHTMLVideo","getContextForMediaHTML","posterSource","getMediaHTMLAudio","trackSource","kind","trackDefault","checked","filter","description","showControls","mediaControl","mediaAutoplay","mediaMute","mediaLoop","getFilepickerTypeFromElement","e","target","actions","mediaBrowser","preventDefault","trackDefaultAction","getKind","el","parentElement","select","event","modal","html","getRoot","outerHTML","insertContent","getBody","$root","root","addEventListener","clickHandler","bind","on","ModalEvents","save","handleDialogueSubmission","hidden","destroy","shown","dropdown","defaultVal"],"mappings":"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"} \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/embedmodal.min.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/embedmodal.min.js new file mode 100755 index 00000000..70ad4aa9 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/embedmodal.min.js @@ -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 \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/embedmodal.min.js.map b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/embedmodal.min.js.map new file mode 100755 index 00000000..d51705a6 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/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 .\n\n/**\n * Embedded Media Management Modal for Tiny.\n *\n * @module tiny_mediacms/embedmodal\n * @copyright 2022 Andrew Lyons \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"} \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/iframeembed.min.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/iframeembed.min.js new file mode 100755 index 00000000..4b79b677 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/iframeembed.min.js @@ -0,0 +1,3 @@ +define("tiny_mediacms/iframeembed",["exports","core/templates","core/str","core/modal_events","./common","./iframemodal","./selectors","./options"],(function(_exports,_templates,_str,ModalEvents,_common,_iframemodal,_selectors,_options){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),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),_iframemodal=_interopRequireDefault(_iframemodal),_selectors=_interopRequireDefault(_selectors);return _exports.default=class{constructor(editor){_defineProperty(this,"editor",null),_defineProperty(this,"currentModal",null),_defineProperty(this,"isUpdating",!1),_defineProperty(this,"selectedIframe",null),_defineProperty(this,"debounceTimer",null),_defineProperty(this,"iframeLibraryLoaded",!1),_defineProperty(this,"selectedLibraryVideo",null),_defineProperty(this,"iframeLibraryUrl","https://temp.web357.com/mediacms/deic-mediacms-embed-videos.html"),this.editor=editor}parseInput(input){if(!input||!input.trim())return null;const iframeMatch=(input=input.trim()).match(/]*src=["']([^"']+)["'][^>]*>/i);return iframeMatch?this.parseEmbedUrl(iframeMatch[1]):input.startsWith("http://")||input.startsWith("https://")?this.parseVideoUrl(input):null}parseVideoUrl(url){try{const urlObj=new URL(url),baseUrl="".concat(urlObj.protocol,"//").concat(urlObj.host);if("/view"===urlObj.pathname&&urlObj.searchParams.has("m"))return{baseUrl:baseUrl,videoId:urlObj.searchParams.get("m"),isEmbed:!1};if("/embed"===urlObj.pathname&&urlObj.searchParams.has("m")){const tParam=urlObj.searchParams.get("t");return{baseUrl:baseUrl,videoId:urlObj.searchParams.get("m"),isEmbed:!0,showTitle:"1"===urlObj.searchParams.get("showTitle"),linkTitle:"1"===urlObj.searchParams.get("linkTitle"),showRelated:"1"===urlObj.searchParams.get("showRelated"),showUserAvatar:"1"===urlObj.searchParams.get("showUserAvatar"),startAt:tParam?this.secondsToTimeString(parseInt(tParam)):null}}return{baseUrl:baseUrl,rawUrl:url,isGeneric:!0}}catch(e){return null}}parseEmbedUrl(url){return this.parseVideoUrl(url)}secondsToTimeString(seconds){const mins=Math.floor(seconds/60),secs=seconds%60;return"".concat(mins,":").concat(secs.toString().padStart(2,"0"))}timeStringToSeconds(timeStr){if(!timeStr||!timeStr.trim())return null;if((timeStr=timeStr.trim()).includes(":")){const parts=timeStr.split(":");return 60*(parseInt(parts[0])||0)+(parseInt(parts[1])||0)}const secs=parseInt(timeStr);return isNaN(secs)?null:secs}buildEmbedUrl(parsed,options){if(parsed.isGeneric)return parsed.rawUrl;const url=new URL("".concat(parsed.baseUrl,"/embed"));if(url.searchParams.set("m",parsed.videoId),url.searchParams.set("showTitle",options.showTitle?"1":"0"),url.searchParams.set("showRelated",options.showRelated?"1":"0"),url.searchParams.set("showUserAvatar",options.showUserAvatar?"1":"0"),url.searchParams.set("linkTitle",options.linkTitle?"1":"0"),options.startAtEnabled&&options.startAt){const seconds=this.timeStringToSeconds(options.startAt);null!==seconds&&seconds>0&&url.searchParams.set("t",seconds.toString())}return url.toString()}async getTemplateContext(){let data=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return{elementid:this.editor.getElement().id,isupdating:this.isUpdating,url:data.url||"",showTitle:!1!==data.showTitle,linkTitle:!1!==data.linkTitle,showRelated:!1!==data.showRelated,showUserAvatar:!1!==data.showUserAvatar,responsive:!1!==data.responsive,startAtEnabled:data.startAtEnabled||!1,startAt:data.startAt||"0:00",width:data.width||560,height:data.height||315,is16_9:!data.aspectRatio||"16:9"===data.aspectRatio,is4_3:"4:3"===data.aspectRatio,is1_1:"1:1"===data.aspectRatio,isCustom:"custom"===data.aspectRatio}}async displayDialogue(){this.selectedIframe=this.getSelectedIframe();const data=this.getCurrentIframeData();this.isUpdating=null!==data,this.iframeLibraryLoaded=!1,this.currentModal=await _iframemodal.default.create({title:(0,_str.getString)("iframemodaltitle",_common.component),templateContext:await this.getTemplateContext(data||{})}),await this.registerEventListeners(this.currentModal)}getSelectedIframe(){const node=this.editor.selection.getNode();if("iframe"===node.nodeName.toLowerCase())return node;const iframe=node.querySelector("iframe");if(iframe)return iframe;const wrapper=node.closest(".tiny-mediacms-iframe-wrapper")||node.closest(".tiny-iframe-responsive");return wrapper?wrapper.querySelector("iframe"):null}getCurrentIframeData(){var _parsed$showTitle,_parsed$linkTitle,_parsed$showRelated,_parsed$showUserAvata;if(!this.selectedIframe)return null;const src=this.selectedIframe.getAttribute("src"),parsed=this.parseInput(src),isResponsive=(this.selectedIframe.getAttribute("style")||"").includes("aspect-ratio");return{url:src,width:this.selectedIframe.getAttribute("width")||560,height:this.selectedIframe.getAttribute("height")||315,showTitle:null===(_parsed$showTitle=null==parsed?void 0:parsed.showTitle)||void 0===_parsed$showTitle||_parsed$showTitle,linkTitle:null===(_parsed$linkTitle=null==parsed?void 0:parsed.linkTitle)||void 0===_parsed$linkTitle||_parsed$linkTitle,showRelated:null===(_parsed$showRelated=null==parsed?void 0:parsed.showRelated)||void 0===_parsed$showRelated||_parsed$showRelated,showUserAvatar:null===(_parsed$showUserAvata=null==parsed?void 0:parsed.showUserAvatar)||void 0===_parsed$showUserAvata||_parsed$showUserAvata,responsive:isResponsive,startAtEnabled:null!==(null==parsed?void 0:parsed.startAt),startAt:(null==parsed?void 0:parsed.startAt)||"0:00"}}getFormValues(root){const form=root.querySelector(_selectors.default.IFRAME.elements.form);return{url:form.querySelector(_selectors.default.IFRAME.elements.url).value.trim(),showTitle:form.querySelector(_selectors.default.IFRAME.elements.showTitle).checked,linkTitle:form.querySelector(_selectors.default.IFRAME.elements.linkTitle).checked,showRelated:form.querySelector(_selectors.default.IFRAME.elements.showRelated).checked,showUserAvatar:form.querySelector(_selectors.default.IFRAME.elements.showUserAvatar).checked,responsive:form.querySelector(_selectors.default.IFRAME.elements.responsive).checked,startAtEnabled:form.querySelector(_selectors.default.IFRAME.elements.startAtEnabled).checked,startAt:form.querySelector(_selectors.default.IFRAME.elements.startAt).value.trim(),aspectRatio:form.querySelector(_selectors.default.IFRAME.elements.aspectRatio).value,width:parseInt(form.querySelector(_selectors.default.IFRAME.elements.width).value)||560,height:parseInt(form.querySelector(_selectors.default.IFRAME.elements.height).value)||315}}async generateIframeHtml(values){const parsed=this.parseInput(values.url);if(!parsed)return"";const embedUrl=this.buildEmbedUrl(parsed,values),aspectRatioCalcs={"16:9":"16 / 9","4:3":"4 / 3","1:1":"1 / 1",custom:"".concat(values.width," / ").concat(values.height)},context={src:embedUrl,width:values.width,height:values.height,responsive:values.responsive,aspectRatioCalc:aspectRatioCalcs[values.aspectRatio]||"16 / 9",aspectRatioValue:aspectRatioCalcs[values.aspectRatio]||"16 / 9"},{html:html}=await _templates.default.renderForPromise("tiny_mediacms/iframe_embed_output",context);return html}async updatePreview(root){const values=this.getFormValues(root),previewContainer=root.querySelector(_selectors.default.IFRAME.elements.preview),urlWarning=root.querySelector(_selectors.default.IFRAME.elements.urlWarning);if(!values.url)return previewContainer.innerHTML='Enter a video URL to see preview',void urlWarning.classList.add("d-none");const parsed=this.parseInput(values.url);if(!parsed)return previewContainer.innerHTML='Invalid URL format',void urlWarning.classList.remove("d-none");urlWarning.classList.add("d-none");const embedUrl=this.buildEmbedUrl(parsed,values),previewWidth=Math.min(values.width,400),scale=previewWidth/values.width,previewHeight=Math.round(values.height*scale);previewContainer.innerHTML='\n \n ')}handleInputChange(root){clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout((()=>{this.updatePreview(root)}),500)}handleAspectRatioChange(root){const form=root.querySelector(_selectors.default.IFRAME.elements.form),aspectRatio=form.querySelector(_selectors.default.IFRAME.elements.aspectRatio).value,dimensions=_selectors.default.IFRAME.aspectRatios[aspectRatio];dimensions&&"custom"!==aspectRatio&&(form.querySelector(_selectors.default.IFRAME.elements.width).value=dimensions.width,form.querySelector(_selectors.default.IFRAME.elements.height).value=dimensions.height),this.updatePreview(root)}async handleDialogueSubmission(modal){const root=modal.getRoot()[0],values=this.getFormValues(root);if(!values.url)return;const html=await this.generateIframeHtml(values);if(html)if(this.isUpdating&&this.selectedIframe){const wrapper=this.selectedIframe.closest(".tiny-mediacms-iframe-wrapper")||this.selectedIframe.closest(".tiny-iframe-responsive");wrapper?wrapper.outerHTML=html:this.selectedIframe.outerHTML=html,this.isUpdating=!1,this.editor.fire("Change")}else this.editor.insertContent(html)}async handleRemove(modal){const confirmMessage=await(0,_str.getString)("removeiframeconfirm",_common.component);if(window.confirm(confirmMessage)){if(this.selectedIframe){const wrapper=this.selectedIframe.closest(".tiny-mediacms-iframe-wrapper")||this.selectedIframe.closest(".tiny-iframe-responsive");wrapper?wrapper.remove():this.selectedIframe.remove()}this.isUpdating=!1,modal.hide()}}async registerEventListeners(modal){await modal.getBody();const $root=modal.getRoot(),root=$root[0],form=root.querySelector(_selectors.default.IFRAME.elements.form);form.querySelector(_selectors.default.IFRAME.elements.url).addEventListener("input",(()=>this.handleInputChange(root))),[_selectors.default.IFRAME.elements.showTitle,_selectors.default.IFRAME.elements.linkTitle,_selectors.default.IFRAME.elements.showRelated,_selectors.default.IFRAME.elements.showUserAvatar,_selectors.default.IFRAME.elements.responsive,_selectors.default.IFRAME.elements.startAtEnabled].forEach((selector=>{form.querySelector(selector).addEventListener("change",(()=>this.handleInputChange(root)))})),form.querySelector(_selectors.default.IFRAME.elements.startAt).addEventListener("input",(()=>this.handleInputChange(root))),form.querySelector(_selectors.default.IFRAME.elements.aspectRatio).addEventListener("change",(()=>this.handleAspectRatioChange(root))),form.querySelector(_selectors.default.IFRAME.elements.width).addEventListener("input",(()=>this.handleInputChange(root))),form.querySelector(_selectors.default.IFRAME.elements.height).addEventListener("input",(()=>this.handleInputChange(root))),$root.on(ModalEvents.save,(()=>this.handleDialogueSubmission(modal))),$root.on(ModalEvents.hidden,(()=>{this.currentModal.destroy()}));const removeBtn=root.querySelector(_selectors.default.IFRAME.actions.remove);removeBtn&&removeBtn.addEventListener("click",(()=>this.handleRemove(modal)));form.querySelector(_selectors.default.IFRAME.elements.url).value&&this.updatePreview(root);const iframeLibraryTabBtn=form.querySelector(_selectors.default.IFRAME.elements.tabIframeLibraryBtn);if(iframeLibraryTabBtn){iframeLibraryTabBtn.addEventListener("click",(e=>{e.preventDefault(),e.stopPropagation(),this.switchToIframeLibraryTab(root),setTimeout((()=>this.handleIframeLibraryTabClick(root)),100)})),iframeLibraryTabBtn.addEventListener("shown.bs.tab",(()=>this.handleIframeLibraryTabClick(root)));const $iframeLibraryTabBtn=window.jQuery?window.jQuery(iframeLibraryTabBtn):null;$iframeLibraryTabBtn&&$iframeLibraryTabBtn.on("shown.bs.tab",(()=>this.handleIframeLibraryTabClick(root)))}const urlTabBtn=form.querySelector(_selectors.default.IFRAME.elements.tabUrlBtn);urlTabBtn&&urlTabBtn.addEventListener("click",(e=>{e.preventDefault(),e.stopPropagation(),this.switchToUrlTab(root)})),this.registerIframeLibraryEventListeners(root)}switchToUrlTab(root){const form=root.querySelector(_selectors.default.IFRAME.elements.form),urlTabBtn=form.querySelector(_selectors.default.IFRAME.elements.tabUrlBtn),iframeLibraryTabBtn=form.querySelector(_selectors.default.IFRAME.elements.tabIframeLibraryBtn),urlPane=form.querySelector(_selectors.default.IFRAME.elements.paneUrl),iframeLibraryPane=form.querySelector(_selectors.default.IFRAME.elements.paneIframeLibrary);urlTabBtn&&(urlTabBtn.classList.add("active"),urlTabBtn.setAttribute("aria-selected","true")),iframeLibraryTabBtn&&(iframeLibraryTabBtn.classList.remove("active"),iframeLibraryTabBtn.setAttribute("aria-selected","false")),urlPane&&urlPane.classList.add("show","active"),iframeLibraryPane&&iframeLibraryPane.classList.remove("show","active")}switchToIframeLibraryTab(root){const form=root.querySelector(_selectors.default.IFRAME.elements.form),urlTabBtn=form.querySelector(_selectors.default.IFRAME.elements.tabUrlBtn),iframeLibraryTabBtn=form.querySelector(_selectors.default.IFRAME.elements.tabIframeLibraryBtn),urlPane=form.querySelector(_selectors.default.IFRAME.elements.paneUrl),iframeLibraryPane=form.querySelector(_selectors.default.IFRAME.elements.paneIframeLibrary);urlTabBtn&&(urlTabBtn.classList.remove("active"),urlTabBtn.setAttribute("aria-selected","false")),iframeLibraryTabBtn&&(iframeLibraryTabBtn.classList.add("active"),iframeLibraryTabBtn.setAttribute("aria-selected","true")),urlPane&&urlPane.classList.remove("show","active"),iframeLibraryPane&&iframeLibraryPane.classList.add("show","active")}registerIframeLibraryEventListeners(root){window.addEventListener("message",(event=>{this.handleIframeLibraryMessage(root,event)}))}handleIframeLibraryTabClick(root){const pane=root.querySelector(_selectors.default.IFRAME.elements.form).querySelector(_selectors.default.IFRAME.elements.paneIframeLibrary),iframeEl=pane?pane.querySelector(_selectors.default.IFRAME.elements.iframeLibraryFrame):null;console.log("handleIframeLibraryTabClick called, iframeLibraryLoaded:",this.iframeLibraryLoaded,"iframe src:",iframeEl?iframeEl.src:"no iframe","pane:",pane),this.iframeLibraryLoaded&&(!iframeEl||iframeEl.src&&""!==iframeEl.src)||this.loadIframeLibrary(root)}loadIframeLibrary(root){const ltiConfig=(0,_options.getLti)(this.editor);console.log("loadIframeLibrary called, LTI config:",ltiConfig),null!=ltiConfig&<iConfig.contentItemUrl?this.loadIframeLibraryViaLti(root,ltiConfig):this.loadIframeLibraryStatic(root)}loadIframeLibraryViaLti(root,ltiConfig){console.log("loadIframeLibraryViaLti called, config:",ltiConfig);const pane=root.querySelector(_selectors.default.IFRAME.elements.form).querySelector(_selectors.default.IFRAME.elements.paneIframeLibrary);if(!pane)return void console.log("paneIframeLibrary not found!");const placeholderEl=pane.querySelector(_selectors.default.IFRAME.elements.iframeLibraryPlaceholder),loadingEl=pane.querySelector(_selectors.default.IFRAME.elements.iframeLibraryLoading),iframeEl=pane.querySelector(_selectors.default.IFRAME.elements.iframeLibraryFrame);if(!iframeEl)return void console.log("iframeEl not found!");placeholderEl&&placeholderEl.classList.add("d-none"),loadingEl&&loadingEl.classList.remove("d-none"),iframeEl.classList.add("d-none");iframeEl.addEventListener("load",(()=>{console.log("LTI iframe loaded"),this.handleIframeLibraryLoad(root)})),console.log("Setting iframe src to LTI content item URL:",ltiConfig.contentItemUrl),iframeEl.src=ltiConfig.contentItemUrl}loadIframeLibraryStatic(root){console.log("loadIframeLibraryStatic called, URL:",this.iframeLibraryUrl);const pane=root.querySelector(_selectors.default.IFRAME.elements.form).querySelector(_selectors.default.IFRAME.elements.paneIframeLibrary);if(!pane)return void console.log("paneIframeLibrary not found!");const placeholderEl=pane.querySelector(_selectors.default.IFRAME.elements.iframeLibraryPlaceholder),loadingEl=pane.querySelector(_selectors.default.IFRAME.elements.iframeLibraryLoading),iframeEl=pane.querySelector(_selectors.default.IFRAME.elements.iframeLibraryFrame);if(console.log("Elements found:",{placeholderEl:placeholderEl,loadingEl:loadingEl,iframeEl:iframeEl}),!iframeEl)return void console.log("iframeEl not found!");placeholderEl&&placeholderEl.classList.add("d-none"),loadingEl&&loadingEl.classList.remove("d-none"),iframeEl.classList.add("d-none");const loadHandler=()=>{console.log("iframe loaded, src:",iframeEl.src),iframeEl.src===this.iframeLibraryUrl&&(this.handleIframeLibraryLoad(root),iframeEl.removeEventListener("load",loadHandler))};iframeEl.addEventListener("load",loadHandler),iframeEl.src=this.iframeLibraryUrl,console.log("iframe src set to:",iframeEl.src)}handleIframeLibraryLoad(root){console.log("handleIframeLibraryLoad called");const pane=root.querySelector(_selectors.default.IFRAME.elements.form).querySelector(_selectors.default.IFRAME.elements.paneIframeLibrary);if(!pane)return;const placeholderEl=pane.querySelector(_selectors.default.IFRAME.elements.iframeLibraryPlaceholder),loadingEl=pane.querySelector(_selectors.default.IFRAME.elements.iframeLibraryLoading),iframeEl=pane.querySelector(_selectors.default.IFRAME.elements.iframeLibraryFrame);placeholderEl&&placeholderEl.classList.add("d-none"),loadingEl&&loadingEl.classList.add("d-none"),iframeEl&&iframeEl.classList.remove("d-none"),this.iframeLibraryLoaded=!0}handleIframeLibraryMessage(root,event){console.log("handleIframeLibraryMessage received:",event.data,"from origin:",event.origin);const data=event.data;if(data){if("videoSelected"===data.type&&data.embedUrl)return console.log("Video selected (videoSelected):",data.embedUrl,"videoId:",data.videoId),void this.selectIframeLibraryVideo(root,data.embedUrl,data.videoId);if("ltiDeepLinkingResponse"!==data.type&&"LtiDeepLinkingResponse"!==data.messageType)if("selectMedia"!==data.action&&"mediaSelected"!==data.action);else{const embedUrl=data.embedUrl||data.url||"",videoId=data.mediaId||data.videoId||data.id||"";embedUrl&&(console.log("Video selected (mediaSelected):",embedUrl,"videoId:",videoId),this.selectIframeLibraryVideo(root,embedUrl,videoId))}else{console.log("LTI Deep Linking response received:",data);const contentItems=data.content_items||data.contentItems||[];if(contentItems.length>0){const item=contentItems[0],embedUrl=item.url||item.embed_url||item.embedUrl||"",videoId=item.id||item.mediaId||"";embedUrl&&(console.log("Video selected (LTI):",embedUrl,"videoId:",videoId),this.selectIframeLibraryVideo(root,embedUrl,videoId))}}}}selectIframeLibraryVideo(root,embedUrl,videoId){root.querySelector(_selectors.default.IFRAME.elements.form).querySelector(_selectors.default.IFRAME.elements.url).value=embedUrl,this.switchToUrlTab(root),this.updatePreview(root),this.selectedLibraryVideo={embedUrl:embedUrl,videoId:videoId}}},_exports.default})); + +//# sourceMappingURL=iframeembed.min.js.map \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/iframeembed.min.js.map b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/iframeembed.min.js.map new file mode 100755 index 00000000..b673a5f1 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/iframeembed.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"iframeembed.min.js","sources":["../src/iframeembed.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 .\n\n/**\n * Tiny Media2 Iframe Embed class.\n *\n * @module tiny_mediacms/iframeembed\n * @copyright 2024\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 * as ModalEvents from 'core/modal_events';\nimport { component } from './common';\nimport IframeModal from './iframemodal';\nimport Selectors from './selectors';\nimport { getLti } from './options';\n\nexport default class IframeEmbed {\n editor = null;\n currentModal = null;\n isUpdating = false;\n selectedIframe = null;\n debounceTimer = null;\n // Iframe library state\n iframeLibraryLoaded = false;\n selectedLibraryVideo = null;\n iframeLibraryUrl =\n 'https://temp.web357.com/mediacms/deic-mediacms-embed-videos.html';\n\n constructor(editor) {\n this.editor = editor;\n }\n\n /**\n * Parse input to extract video URL or iframe src.\n * Handles: iframe embed code, view URL, or embed URL.\n *\n * @param {string} input - The user input (URL or embed code)\n * @returns {Object|null} Parsed URL info or null if invalid\n */\n parseInput(input) {\n if (!input || !input.trim()) {\n return null;\n }\n\n input = input.trim();\n\n // Check if it's an iframe embed code\n const iframeMatch = input.match(\n /]*src=[\"']([^\"']+)[\"'][^>]*>/i,\n );\n if (iframeMatch) {\n return this.parseEmbedUrl(iframeMatch[1]);\n }\n\n // Check if it's a URL\n if (input.startsWith('http://') || input.startsWith('https://')) {\n return this.parseVideoUrl(input);\n }\n\n return null;\n }\n\n /**\n * Parse a video view URL and convert to embed format.\n *\n * @param {string} url - The video URL\n * @returns {Object|null} Parsed info\n */\n parseVideoUrl(url) {\n try {\n const urlObj = new URL(url);\n const baseUrl = `${urlObj.protocol}//${urlObj.host}`;\n\n // MediaCMS view URL: /view?m=VIDEO_ID\n if (urlObj.pathname === '/view' && urlObj.searchParams.has('m')) {\n return {\n baseUrl: baseUrl,\n videoId: urlObj.searchParams.get('m'),\n isEmbed: false,\n };\n }\n\n // MediaCMS embed URL: /embed?m=VIDEO_ID&options\n if (urlObj.pathname === '/embed' && urlObj.searchParams.has('m')) {\n const tParam = urlObj.searchParams.get('t');\n return {\n baseUrl: baseUrl,\n videoId: urlObj.searchParams.get('m'),\n isEmbed: true,\n showTitle: urlObj.searchParams.get('showTitle') === '1',\n linkTitle: urlObj.searchParams.get('linkTitle') === '1',\n showRelated: urlObj.searchParams.get('showRelated') === '1',\n showUserAvatar:\n urlObj.searchParams.get('showUserAvatar') === '1',\n startAt: tParam\n ? this.secondsToTimeString(parseInt(tParam))\n : null,\n };\n }\n\n // Generic URL - just use as-is\n return {\n baseUrl: baseUrl,\n rawUrl: url,\n isGeneric: true,\n };\n } catch (e) {\n return null;\n }\n }\n\n /**\n * Parse an embed URL.\n *\n * @param {string} url - The embed URL\n * @returns {Object|null} Parsed info\n */\n parseEmbedUrl(url) {\n return this.parseVideoUrl(url);\n }\n\n /**\n * Convert seconds to time string (M:SS format).\n *\n * @param {number} seconds - Time in seconds\n * @returns {string} Time string\n */\n secondsToTimeString(seconds) {\n const mins = Math.floor(seconds / 60);\n const secs = seconds % 60;\n return `${mins}:${secs.toString().padStart(2, '0')}`;\n }\n\n /**\n * Convert time string to seconds.\n *\n * @param {string} timeStr - Time string (M:SS or just seconds)\n * @returns {number|null} Seconds or null if invalid\n */\n timeStringToSeconds(timeStr) {\n if (!timeStr || !timeStr.trim()) {\n return null;\n }\n timeStr = timeStr.trim();\n\n // Handle M:SS format\n if (timeStr.includes(':')) {\n const parts = timeStr.split(':');\n const mins = parseInt(parts[0]) || 0;\n const secs = parseInt(parts[1]) || 0;\n return mins * 60 + secs;\n }\n\n // Handle plain seconds\n const secs = parseInt(timeStr);\n return isNaN(secs) ? null : secs;\n }\n\n /**\n * Build the embed URL with options.\n *\n * @param {Object} parsed - Parsed URL info\n * @param {Object} options - Embed options\n * @returns {string} The complete embed URL\n */\n buildEmbedUrl(parsed, options) {\n if (parsed.isGeneric) {\n return parsed.rawUrl;\n }\n\n const url = new URL(`${parsed.baseUrl}/embed`);\n url.searchParams.set('m', parsed.videoId);\n\n // Always include all options with 1 or 0\n url.searchParams.set('showTitle', options.showTitle ? '1' : '0');\n url.searchParams.set('showRelated', options.showRelated ? '1' : '0');\n url.searchParams.set(\n 'showUserAvatar',\n options.showUserAvatar ? '1' : '0',\n );\n url.searchParams.set('linkTitle', options.linkTitle ? '1' : '0');\n\n // Add start time if enabled\n if (options.startAtEnabled && options.startAt) {\n const seconds = this.timeStringToSeconds(options.startAt);\n if (seconds !== null && seconds > 0) {\n url.searchParams.set('t', seconds.toString());\n }\n }\n\n return url.toString();\n }\n\n /**\n * Get the template context for the modal.\n *\n * @param {Object} data - Existing data for updating\n * @returns {Object} Template context\n */\n async getTemplateContext(data = {}) {\n return {\n elementid: this.editor.getElement().id,\n isupdating: this.isUpdating,\n url: data.url || '',\n showTitle: data.showTitle !== false,\n linkTitle: data.linkTitle !== false,\n showRelated: data.showRelated !== false,\n showUserAvatar: data.showUserAvatar !== false,\n responsive: data.responsive !== false,\n startAtEnabled: data.startAtEnabled || false,\n startAt: data.startAt || '0:00',\n width: data.width || 560,\n height: data.height || 315,\n is16_9: !data.aspectRatio || data.aspectRatio === '16:9',\n is4_3: data.aspectRatio === '4:3',\n is1_1: data.aspectRatio === '1:1',\n isCustom: data.aspectRatio === 'custom',\n };\n }\n\n /**\n * Display the iframe embed dialog.\n */\n async displayDialogue() {\n this.selectedIframe = this.getSelectedIframe();\n const data = this.getCurrentIframeData();\n this.isUpdating = data !== null;\n\n // Reset iframe library state for new modal\n this.iframeLibraryLoaded = false;\n\n this.currentModal = await IframeModal.create({\n title: getString('iframemodaltitle', component),\n templateContext: await this.getTemplateContext(data || {}),\n });\n\n await this.registerEventListeners(this.currentModal);\n }\n\n /**\n * Get the currently selected iframe in the editor.\n *\n * @returns {HTMLElement|null} The iframe element or null\n */\n getSelectedIframe() {\n const node = this.editor.selection.getNode();\n\n if (node.nodeName.toLowerCase() === 'iframe') {\n return node;\n }\n\n // Check if selection contains an iframe wrapper (including overlay wrapper)\n const iframe = node.querySelector('iframe');\n if (iframe) {\n return iframe;\n }\n\n // Check if we're on the overlay or wrapper and need to find the iframe\n const wrapper =\n node.closest('.tiny-mediacms-iframe-wrapper') ||\n node.closest('.tiny-iframe-responsive');\n if (wrapper) {\n return wrapper.querySelector('iframe');\n }\n\n return null;\n }\n\n /**\n * Get current iframe data for editing.\n *\n * @returns {Object|null} Current iframe data or null\n */\n getCurrentIframeData() {\n if (!this.selectedIframe) {\n return null;\n }\n\n const src = this.selectedIframe.getAttribute('src');\n const parsed = this.parseInput(src);\n\n // Check if responsive by looking at style\n const style = this.selectedIframe.getAttribute('style') || '';\n const isResponsive = style.includes('aspect-ratio');\n\n return {\n url: src,\n width: this.selectedIframe.getAttribute('width') || 560,\n height: this.selectedIframe.getAttribute('height') || 315,\n showTitle: parsed?.showTitle ?? true,\n linkTitle: parsed?.linkTitle ?? true,\n showRelated: parsed?.showRelated ?? true,\n showUserAvatar: parsed?.showUserAvatar ?? true,\n responsive: isResponsive,\n startAtEnabled: parsed?.startAt !== null,\n startAt: parsed?.startAt || '0:00',\n };\n }\n\n /**\n * Get form values from the modal.\n *\n * @param {HTMLElement} root - Modal root element\n * @returns {Object} Form values\n */\n getFormValues(root) {\n const form = root.querySelector(Selectors.IFRAME.elements.form);\n\n return {\n url: form.querySelector(Selectors.IFRAME.elements.url).value.trim(),\n showTitle: form.querySelector(Selectors.IFRAME.elements.showTitle)\n .checked,\n linkTitle: form.querySelector(Selectors.IFRAME.elements.linkTitle)\n .checked,\n showRelated: form.querySelector(\n Selectors.IFRAME.elements.showRelated,\n ).checked,\n showUserAvatar: form.querySelector(\n Selectors.IFRAME.elements.showUserAvatar,\n ).checked,\n responsive: form.querySelector(Selectors.IFRAME.elements.responsive)\n .checked,\n startAtEnabled: form.querySelector(\n Selectors.IFRAME.elements.startAtEnabled,\n ).checked,\n startAt: form\n .querySelector(Selectors.IFRAME.elements.startAt)\n .value.trim(),\n aspectRatio: form.querySelector(\n Selectors.IFRAME.elements.aspectRatio,\n ).value,\n width:\n parseInt(\n form.querySelector(Selectors.IFRAME.elements.width).value,\n ) || 560,\n height:\n parseInt(\n form.querySelector(Selectors.IFRAME.elements.height).value,\n ) || 315,\n };\n }\n\n /**\n * Generate the iframe HTML.\n *\n * @param {Object} values - Form values\n * @returns {Promise} Generated HTML\n */\n async generateIframeHtml(values) {\n const parsed = this.parseInput(values.url);\n if (!parsed) {\n return '';\n }\n\n const embedUrl = this.buildEmbedUrl(parsed, values);\n\n // Calculate aspect ratio values for CSS\n const aspectRatioCalcs = {\n '16:9': '16 / 9',\n '4:3': '4 / 3',\n '1:1': '1 / 1',\n custom: `${values.width} / ${values.height}`,\n };\n\n const context = {\n src: embedUrl,\n width: values.width,\n height: values.height,\n responsive: values.responsive,\n aspectRatioCalc: aspectRatioCalcs[values.aspectRatio] || '16 / 9',\n aspectRatioValue: aspectRatioCalcs[values.aspectRatio] || '16 / 9',\n };\n\n const { html } = await Templates.renderForPromise(\n 'tiny_mediacms/iframe_embed_output',\n context,\n );\n return html;\n }\n\n /**\n * Update the preview in the modal.\n *\n * @param {HTMLElement} root - Modal root element\n */\n async updatePreview(root) {\n const values = this.getFormValues(root);\n const previewContainer = root.querySelector(\n Selectors.IFRAME.elements.preview,\n );\n const urlWarning = root.querySelector(\n Selectors.IFRAME.elements.urlWarning,\n );\n\n if (!values.url) {\n previewContainer.innerHTML =\n 'Enter a video URL to see preview';\n urlWarning.classList.add('d-none');\n return;\n }\n\n const parsed = this.parseInput(values.url);\n if (!parsed) {\n previewContainer.innerHTML =\n 'Invalid URL format';\n urlWarning.classList.remove('d-none');\n return;\n }\n\n urlWarning.classList.add('d-none');\n const embedUrl = this.buildEmbedUrl(parsed, values);\n\n // Show a scaled preview\n const previewWidth = Math.min(values.width, 400);\n const scale = previewWidth / values.width;\n const previewHeight = Math.round(values.height * scale);\n\n previewContainer.innerHTML = `\n \n `;\n }\n\n /**\n * Handle form input changes with debounce.\n *\n * @param {HTMLElement} root - Modal root element\n */\n handleInputChange(root) {\n clearTimeout(this.debounceTimer);\n this.debounceTimer = setTimeout(() => {\n this.updatePreview(root);\n }, 500);\n }\n\n /**\n * Handle aspect ratio change - update dimensions.\n *\n * @param {HTMLElement} root - Modal root element\n */\n handleAspectRatioChange(root) {\n const form = root.querySelector(Selectors.IFRAME.elements.form);\n const aspectRatio = form.querySelector(\n Selectors.IFRAME.elements.aspectRatio,\n ).value;\n const dimensions = Selectors.IFRAME.aspectRatios[aspectRatio];\n\n // Only update dimensions for predefined ratios, not 'custom'\n if (dimensions && aspectRatio !== 'custom') {\n form.querySelector(Selectors.IFRAME.elements.width).value =\n dimensions.width;\n form.querySelector(Selectors.IFRAME.elements.height).value =\n dimensions.height;\n }\n\n this.updatePreview(root);\n }\n\n /**\n * Handle dialog submission.\n *\n * @param {Object} modal - The modal instance\n */\n async handleDialogueSubmission(modal) {\n const root = modal.getRoot()[0];\n const values = this.getFormValues(root);\n\n if (!values.url) {\n return;\n }\n\n const html = await this.generateIframeHtml(values);\n if (html) {\n if (this.isUpdating && this.selectedIframe) {\n // Replace existing iframe (including wrapper if present)\n // Check for both old wrapper and new overlay wrapper\n const wrapper =\n this.selectedIframe.closest(\n '.tiny-mediacms-iframe-wrapper',\n ) || this.selectedIframe.closest('.tiny-iframe-responsive');\n if (wrapper) {\n wrapper.outerHTML = html;\n } else {\n this.selectedIframe.outerHTML = html;\n }\n this.isUpdating = false;\n // Fire change event to trigger overlay reprocessing\n this.editor.fire('Change');\n } else {\n this.editor.insertContent(html);\n }\n }\n }\n\n /**\n * Handle video removal from the editor.\n *\n * @param {Object} modal - The modal instance\n */\n async handleRemove(modal) {\n // Get confirmation string\n const confirmMessage = await getString(\n 'removeiframeconfirm',\n component,\n );\n\n // Show confirmation dialog\n // eslint-disable-next-line no-alert\n if (!window.confirm(confirmMessage)) {\n return;\n }\n\n if (this.selectedIframe) {\n // Remove the iframe (including wrapper if present)\n const wrapper =\n this.selectedIframe.closest('.tiny-mediacms-iframe-wrapper') ||\n this.selectedIframe.closest('.tiny-iframe-responsive');\n if (wrapper) {\n wrapper.remove();\n } else {\n this.selectedIframe.remove();\n }\n }\n\n // Close the modal\n this.isUpdating = false;\n modal.hide();\n }\n\n /**\n * Register event listeners for the modal.\n *\n * @param {Object} modal - The modal instance\n */\n async registerEventListeners(modal) {\n await modal.getBody();\n const $root = modal.getRoot();\n const root = $root[0];\n\n // Input change handlers\n const form = root.querySelector(Selectors.IFRAME.elements.form);\n\n // URL input\n form.querySelector(Selectors.IFRAME.elements.url).addEventListener(\n 'input',\n () => this.handleInputChange(root),\n );\n\n // Checkbox options\n [\n Selectors.IFRAME.elements.showTitle,\n Selectors.IFRAME.elements.linkTitle,\n Selectors.IFRAME.elements.showRelated,\n Selectors.IFRAME.elements.showUserAvatar,\n Selectors.IFRAME.elements.responsive,\n Selectors.IFRAME.elements.startAtEnabled,\n ].forEach((selector) => {\n form.querySelector(selector).addEventListener('change', () =>\n this.handleInputChange(root),\n );\n });\n\n // Start at time input\n form.querySelector(Selectors.IFRAME.elements.startAt).addEventListener(\n 'input',\n () => this.handleInputChange(root),\n );\n\n // Aspect ratio change\n form.querySelector(\n Selectors.IFRAME.elements.aspectRatio,\n ).addEventListener('change', () => this.handleAspectRatioChange(root));\n\n // Dimension inputs\n form.querySelector(Selectors.IFRAME.elements.width).addEventListener(\n 'input',\n () => this.handleInputChange(root),\n );\n form.querySelector(Selectors.IFRAME.elements.height).addEventListener(\n 'input',\n () => this.handleInputChange(root),\n );\n\n // Modal events\n $root.on(ModalEvents.save, () => this.handleDialogueSubmission(modal));\n $root.on(ModalEvents.hidden, () => {\n this.currentModal.destroy();\n });\n\n // Remove button handler (only present when updating)\n const removeBtn = root.querySelector(Selectors.IFRAME.actions.remove);\n if (removeBtn) {\n removeBtn.addEventListener('click', () => this.handleRemove(modal));\n }\n\n // Initial preview if we have a URL\n const urlInput = form.querySelector(Selectors.IFRAME.elements.url);\n if (urlInput.value) {\n this.updatePreview(root);\n }\n\n // Tab change handler - load iframe library when switching to iframe library tab\n const iframeLibraryTabBtn = form.querySelector(\n Selectors.IFRAME.elements.tabIframeLibraryBtn,\n );\n if (iframeLibraryTabBtn) {\n iframeLibraryTabBtn.addEventListener('click', (e) => {\n e.preventDefault();\n e.stopPropagation();\n\n // Manually handle tab switching for Bootstrap 5\n this.switchToIframeLibraryTab(root);\n\n // Small delay to ensure tab pane is visible before loading iframe\n setTimeout(() => this.handleIframeLibraryTabClick(root), 100);\n });\n // Also handle Bootstrap tab events (if Bootstrap handles it)\n iframeLibraryTabBtn.addEventListener('shown.bs.tab', () =>\n this.handleIframeLibraryTabClick(root),\n );\n // jQuery Bootstrap 4 event\n const $iframeLibraryTabBtn = window.jQuery\n ? window.jQuery(iframeLibraryTabBtn)\n : null;\n if ($iframeLibraryTabBtn) {\n $iframeLibraryTabBtn.on('shown.bs.tab', () =>\n this.handleIframeLibraryTabClick(root),\n );\n }\n }\n\n // Tab change handler for URL tab\n const urlTabBtn = form.querySelector(\n Selectors.IFRAME.elements.tabUrlBtn,\n );\n if (urlTabBtn) {\n urlTabBtn.addEventListener('click', (e) => {\n e.preventDefault();\n e.stopPropagation();\n\n // Manually handle tab switching\n this.switchToUrlTab(root);\n });\n }\n\n // Iframe library event listeners\n this.registerIframeLibraryEventListeners(root);\n }\n\n /**\n * Switch to the URL tab.\n *\n * @param {HTMLElement} root - Modal root element\n */\n switchToUrlTab(root) {\n const form = root.querySelector(Selectors.IFRAME.elements.form);\n\n // Get tab buttons\n const urlTabBtn = form.querySelector(Selectors.IFRAME.elements.tabUrlBtn);\n const iframeLibraryTabBtn = form.querySelector(Selectors.IFRAME.elements.tabIframeLibraryBtn);\n\n // Get tab panes\n const urlPane = form.querySelector(Selectors.IFRAME.elements.paneUrl);\n const iframeLibraryPane = form.querySelector(Selectors.IFRAME.elements.paneIframeLibrary);\n\n // Update tab button states\n if (urlTabBtn) {\n urlTabBtn.classList.add('active');\n urlTabBtn.setAttribute('aria-selected', 'true');\n }\n if (iframeLibraryTabBtn) {\n iframeLibraryTabBtn.classList.remove('active');\n iframeLibraryTabBtn.setAttribute('aria-selected', 'false');\n }\n\n // Update tab pane visibility\n if (urlPane) {\n urlPane.classList.add('show', 'active');\n }\n if (iframeLibraryPane) {\n iframeLibraryPane.classList.remove('show', 'active');\n }\n }\n\n /**\n * Switch to the iframe library tab.\n *\n * @param {HTMLElement} root - Modal root element\n */\n switchToIframeLibraryTab(root) {\n const form = root.querySelector(Selectors.IFRAME.elements.form);\n\n // Get tab buttons\n const urlTabBtn = form.querySelector(Selectors.IFRAME.elements.tabUrlBtn);\n const iframeLibraryTabBtn = form.querySelector(Selectors.IFRAME.elements.tabIframeLibraryBtn);\n\n // Get tab panes\n const urlPane = form.querySelector(Selectors.IFRAME.elements.paneUrl);\n const iframeLibraryPane = form.querySelector(Selectors.IFRAME.elements.paneIframeLibrary);\n\n // Update tab button states\n if (urlTabBtn) {\n urlTabBtn.classList.remove('active');\n urlTabBtn.setAttribute('aria-selected', 'false');\n }\n if (iframeLibraryTabBtn) {\n iframeLibraryTabBtn.classList.add('active');\n iframeLibraryTabBtn.setAttribute('aria-selected', 'true');\n }\n\n // Update tab pane visibility\n if (urlPane) {\n urlPane.classList.remove('show', 'active');\n }\n if (iframeLibraryPane) {\n iframeLibraryPane.classList.add('show', 'active');\n }\n }\n\n /**\n * Register event listeners for the iframe library.\n *\n * @param {HTMLElement} root - Modal root element\n */\n registerIframeLibraryEventListeners(root) {\n // Listen for messages from the iframe (for video selection)\n window.addEventListener('message', (event) => {\n this.handleIframeLibraryMessage(root, event);\n });\n }\n\n /**\n * Handle iframe library tab click - load iframe if not already loaded.\n *\n * @param {HTMLElement} root - Modal root element\n */\n handleIframeLibraryTabClick(root) {\n const form = root.querySelector(Selectors.IFRAME.elements.form);\n const pane = form.querySelector(\n Selectors.IFRAME.elements.paneIframeLibrary,\n );\n const iframeEl = pane\n ? pane.querySelector(Selectors.IFRAME.elements.iframeLibraryFrame)\n : null;\n\n // eslint-disable-next-line no-console\n console.log(\n 'handleIframeLibraryTabClick called, iframeLibraryLoaded:',\n this.iframeLibraryLoaded,\n 'iframe src:',\n iframeEl ? iframeEl.src : 'no iframe',\n 'pane:',\n pane,\n );\n\n // Load if not loaded or iframe has no source or empty source\n if (\n !this.iframeLibraryLoaded ||\n (iframeEl && (!iframeEl.src || iframeEl.src === ''))\n ) {\n this.loadIframeLibrary(root);\n }\n }\n\n /**\n * Load the iframe library using LTI flow or fallback to static URL.\n *\n * @param {HTMLElement} root - Modal root element\n */\n loadIframeLibrary(root) {\n const ltiConfig = getLti(this.editor);\n\n // eslint-disable-next-line no-console\n console.log('loadIframeLibrary called, LTI config:', ltiConfig);\n\n // Check if LTI is configured with a content item URL\n if (ltiConfig?.contentItemUrl) {\n this.loadIframeLibraryViaLti(root, ltiConfig);\n } else {\n // Fallback to static URL if LTI not configured\n this.loadIframeLibraryStatic(root);\n }\n }\n\n /**\n * Load the iframe library via LTI Deep Linking (Content Item Selection).\n * Sets the iframe src to contentitem.php which initiates the LTI Deep Linking flow.\n * This sends an LtiDeepLinkingRequest message, which will redirect to the\n * tool's content selection interface (e.g., /lti/select-media/).\n *\n * @param {HTMLElement} root - Modal root element\n * @param {Object} ltiConfig - LTI configuration\n */\n loadIframeLibraryViaLti(root, ltiConfig) {\n // eslint-disable-next-line no-console\n console.log('loadIframeLibraryViaLti called, config:', ltiConfig);\n\n const form = root.querySelector(Selectors.IFRAME.elements.form);\n const pane = form.querySelector(\n Selectors.IFRAME.elements.paneIframeLibrary,\n );\n\n if (!pane) {\n // eslint-disable-next-line no-console\n console.log('paneIframeLibrary not found!');\n return;\n }\n\n const placeholderEl = pane.querySelector(\n Selectors.IFRAME.elements.iframeLibraryPlaceholder,\n );\n const loadingEl = pane.querySelector(\n Selectors.IFRAME.elements.iframeLibraryLoading,\n );\n const iframeEl = pane.querySelector(\n Selectors.IFRAME.elements.iframeLibraryFrame,\n );\n\n if (!iframeEl) {\n // eslint-disable-next-line no-console\n console.log('iframeEl not found!');\n return;\n }\n\n // Hide placeholder, show loading state\n if (placeholderEl) {\n placeholderEl.classList.add('d-none');\n }\n if (loadingEl) {\n loadingEl.classList.remove('d-none');\n }\n iframeEl.classList.add('d-none');\n\n // Set up load listener - note: this may fire multiple times during LTI redirects\n const loadHandler = () => {\n // eslint-disable-next-line no-console\n console.log('LTI iframe loaded');\n this.handleIframeLibraryLoad(root);\n };\n iframeEl.addEventListener('load', loadHandler);\n\n // Set the iframe src to the content item URL\n // This initiates the LTI Deep Linking flow:\n // 1. contentitem.php initiates OIDC login\n // 2. LTI provider authenticates\n // 3. Moodle sends LtiDeepLinkingRequest\n // 4. Tool provider shows content selection interface (e.g., /lti/select-media/)\n // eslint-disable-next-line no-console\n console.log('Setting iframe src to LTI content item URL:', ltiConfig.contentItemUrl);\n iframeEl.src = ltiConfig.contentItemUrl;\n }\n\n /**\n * Load the iframe library using static URL (fallback).\n *\n * @param {HTMLElement} root - Modal root element\n */\n loadIframeLibraryStatic(root) {\n // eslint-disable-next-line no-console\n console.log(\n 'loadIframeLibraryStatic called, URL:',\n this.iframeLibraryUrl,\n );\n\n const form = root.querySelector(Selectors.IFRAME.elements.form);\n const pane = form.querySelector(\n Selectors.IFRAME.elements.paneIframeLibrary,\n );\n\n if (!pane) {\n // eslint-disable-next-line no-console\n console.log('paneIframeLibrary not found!');\n return;\n }\n\n const placeholderEl = pane.querySelector(\n Selectors.IFRAME.elements.iframeLibraryPlaceholder,\n );\n const loadingEl = pane.querySelector(\n Selectors.IFRAME.elements.iframeLibraryLoading,\n );\n const iframeEl = pane.querySelector(\n Selectors.IFRAME.elements.iframeLibraryFrame,\n );\n\n // eslint-disable-next-line no-console\n console.log('Elements found:', { placeholderEl, loadingEl, iframeEl });\n\n if (!iframeEl) {\n // eslint-disable-next-line no-console\n console.log('iframeEl not found!');\n return;\n }\n\n // Hide placeholder, show loading state\n if (placeholderEl) {\n placeholderEl.classList.add('d-none');\n }\n if (loadingEl) {\n loadingEl.classList.remove('d-none');\n }\n iframeEl.classList.add('d-none');\n\n // Set up load listener before setting src\n const loadHandler = () => {\n // eslint-disable-next-line no-console\n console.log('iframe loaded, src:', iframeEl.src);\n // Only handle if the src matches our target URL\n if (iframeEl.src === this.iframeLibraryUrl) {\n this.handleIframeLibraryLoad(root);\n // Remove the listener after successful load\n iframeEl.removeEventListener('load', loadHandler);\n }\n };\n iframeEl.addEventListener('load', loadHandler);\n\n // Set the iframe source\n iframeEl.src = this.iframeLibraryUrl;\n // eslint-disable-next-line no-console\n console.log('iframe src set to:', iframeEl.src);\n }\n\n /**\n * Handle iframe library load event.\n *\n * @param {HTMLElement} root - Modal root element\n */\n handleIframeLibraryLoad(root) {\n // eslint-disable-next-line no-console\n console.log('handleIframeLibraryLoad called');\n\n const form = root.querySelector(Selectors.IFRAME.elements.form);\n const pane = form.querySelector(\n Selectors.IFRAME.elements.paneIframeLibrary,\n );\n\n if (!pane) {\n return;\n }\n\n const placeholderEl = pane.querySelector(\n Selectors.IFRAME.elements.iframeLibraryPlaceholder,\n );\n const loadingEl = pane.querySelector(\n Selectors.IFRAME.elements.iframeLibraryLoading,\n );\n const iframeEl = pane.querySelector(\n Selectors.IFRAME.elements.iframeLibraryFrame,\n );\n\n // Hide placeholder and loading, show iframe\n if (placeholderEl) {\n placeholderEl.classList.add('d-none');\n }\n if (loadingEl) {\n loadingEl.classList.add('d-none');\n }\n if (iframeEl) {\n iframeEl.classList.remove('d-none');\n }\n\n this.iframeLibraryLoaded = true;\n }\n\n /**\n * Handle messages from the iframe library (for video selection).\n * Supports both custom videoSelected messages and LTI Deep Linking responses.\n *\n * @param {HTMLElement} root - Modal root element\n * @param {MessageEvent} event - The message event\n */\n handleIframeLibraryMessage(root, event) {\n // eslint-disable-next-line no-console\n console.log(\n 'handleIframeLibraryMessage received:',\n event.data,\n 'from origin:',\n event.origin,\n );\n\n const data = event.data;\n\n if (!data) {\n return;\n }\n\n // Handle custom videoSelected message format (from static iframe or custom MediaCMS implementation)\n if (data.type === 'videoSelected' && data.embedUrl) {\n // eslint-disable-next-line no-console\n console.log(\n 'Video selected (videoSelected):',\n data.embedUrl,\n 'videoId:',\n data.videoId,\n );\n this.selectIframeLibraryVideo(root, data.embedUrl, data.videoId);\n return;\n }\n\n // Handle LTI Deep Linking response format\n if (\n data.type === 'ltiDeepLinkingResponse' ||\n data.messageType === 'LtiDeepLinkingResponse'\n ) {\n // eslint-disable-next-line no-console\n console.log('LTI Deep Linking response received:', data);\n const contentItems = data.content_items || data.contentItems || [];\n if (contentItems.length > 0) {\n const item = contentItems[0];\n // Extract embed URL from the content item\n const embedUrl =\n item.url || item.embed_url || item.embedUrl || '';\n const videoId = item.id || item.mediaId || '';\n if (embedUrl) {\n // eslint-disable-next-line no-console\n console.log(\n 'Video selected (LTI):',\n embedUrl,\n 'videoId:',\n videoId,\n );\n this.selectIframeLibraryVideo(root, embedUrl, videoId);\n }\n }\n return;\n }\n\n // Handle MediaCMS specific message format (if different from above)\n if (data.action === 'selectMedia' || data.action === 'mediaSelected') {\n const embedUrl = data.embedUrl || data.url || '';\n const videoId = data.mediaId || data.videoId || data.id || '';\n if (embedUrl) {\n // eslint-disable-next-line no-console\n console.log(\n 'Video selected (mediaSelected):',\n embedUrl,\n 'videoId:',\n videoId,\n );\n this.selectIframeLibraryVideo(root, embedUrl, videoId);\n }\n return;\n }\n }\n\n /**\n * Select a video from the iframe library and populate the URL field.\n *\n * @param {HTMLElement} root - Modal root element\n * @param {string} embedUrl - The embed URL for the video\n * @param {string} videoId - The video ID\n */\n selectIframeLibraryVideo(root, embedUrl, videoId) {\n const form = root.querySelector(Selectors.IFRAME.elements.form);\n\n // Populate the URL field\n const urlInput = form.querySelector(Selectors.IFRAME.elements.url);\n urlInput.value = embedUrl;\n\n // Switch to the URL tab using our method\n this.switchToUrlTab(root);\n\n // Update the preview\n this.updatePreview(root);\n\n // Store the selected video\n this.selectedLibraryVideo = { embedUrl, videoId };\n }\n}\n"],"names":["constructor","editor","parseInput","input","trim","iframeMatch","match","this","parseEmbedUrl","startsWith","parseVideoUrl","url","urlObj","URL","baseUrl","protocol","host","pathname","searchParams","has","videoId","get","isEmbed","tParam","showTitle","linkTitle","showRelated","showUserAvatar","startAt","secondsToTimeString","parseInt","rawUrl","isGeneric","e","seconds","mins","Math","floor","secs","toString","padStart","timeStringToSeconds","timeStr","includes","parts","split","isNaN","buildEmbedUrl","parsed","options","set","startAtEnabled","data","elementid","getElement","id","isupdating","isUpdating","responsive","width","height","is16_9","aspectRatio","is4_3","is1_1","isCustom","selectedIframe","getSelectedIframe","getCurrentIframeData","iframeLibraryLoaded","currentModal","IframeModal","create","title","component","templateContext","getTemplateContext","registerEventListeners","node","selection","getNode","nodeName","toLowerCase","iframe","querySelector","wrapper","closest","src","getAttribute","isResponsive","getFormValues","root","form","Selectors","IFRAME","elements","value","checked","values","embedUrl","aspectRatioCalcs","custom","context","aspectRatioCalc","aspectRatioValue","html","Templates","renderForPromise","previewContainer","preview","urlWarning","innerHTML","classList","add","remove","previewWidth","min","scale","previewHeight","round","handleInputChange","clearTimeout","debounceTimer","setTimeout","updatePreview","handleAspectRatioChange","dimensions","aspectRatios","modal","getRoot","generateIframeHtml","outerHTML","fire","insertContent","confirmMessage","window","confirm","hide","getBody","$root","addEventListener","forEach","selector","on","ModalEvents","save","handleDialogueSubmission","hidden","destroy","removeBtn","actions","handleRemove","iframeLibraryTabBtn","tabIframeLibraryBtn","preventDefault","stopPropagation","switchToIframeLibraryTab","handleIframeLibraryTabClick","$iframeLibraryTabBtn","jQuery","urlTabBtn","tabUrlBtn","switchToUrlTab","registerIframeLibraryEventListeners","urlPane","paneUrl","iframeLibraryPane","paneIframeLibrary","setAttribute","event","handleIframeLibraryMessage","pane","iframeEl","iframeLibraryFrame","console","log","loadIframeLibrary","ltiConfig","contentItemUrl","loadIframeLibraryViaLti","loadIframeLibraryStatic","placeholderEl","iframeLibraryPlaceholder","loadingEl","iframeLibraryLoading","handleIframeLibraryLoad","iframeLibraryUrl","loadHandler","removeEventListener","origin","type","selectIframeLibraryVideo","messageType","action","mediaId","contentItems","content_items","length","item","embed_url","selectedLibraryVideo"],"mappings":"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"} \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/iframemodal.min.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/iframemodal.min.js new file mode 100755 index 00000000..178a27eb --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/iframemodal.min.js @@ -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 \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/iframemodal.min.js.map b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/iframemodal.min.js.map new file mode 100755 index 00000000..7ab1d3b8 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/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 .\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"} \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/image.min.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/image.min.js new file mode 100755 index 00000000..4e8c1d43 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/image.min.js @@ -0,0 +1,3 @@ +define("tiny_mediacms/image",["exports","./selectors","./imagemodal","./options","editor_tiny/options","tiny_mediacms/imageinsert","tiny_mediacms/imagedetails","core/prefetch","core/str","tiny_mediacms/imagehelpers"],(function(_exports,_selectors,_imagemodal,_options,_options2,_imageinsert,_imagedetails,_prefetch,_str,_imagehelpers){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,_selectors=_interopRequireDefault(_selectors),_imagemodal=_interopRequireDefault(_imagemodal),(0,_prefetch.prefetchStrings)("tiny_mediacms",["imageurlrequired","sizecustom_help"]);return _exports.default=class{constructor(editor){_defineProperty(this,"canShowFilePicker",!1),_defineProperty(this,"editor",null),_defineProperty(this,"currentModal",null),_defineProperty(this,"root",null),_defineProperty(this,"loadInsertImage",(async function(){const templateContext={elementid:this.editor.id,showfilepicker:this.canShowFilePicker,showdropzone:this.canShowDropZone};Promise.all([(0,_imagehelpers.bodyImageInsert)(templateContext,this.root),(0,_imagehelpers.footerImageInsert)(templateContext,this.root)]).then((()=>{new _imageinsert.ImageInsert(this.root,this.editor,this.currentModal,this.canShowFilePicker,this.canShowDropZone).init()})).catch((error=>{window.console.log(error)}))})),_defineProperty(this,"loadPreviewImage",(async function(url){this.startImageLoading();const image=new Image;image.src=url,image.addEventListener("error",(async()=>{this.root.querySelector(_selectors.default.IMAGE.elements.urlWarning).innerHTML=await(0,_str.getString)("imageurlrequired","tiny_mediacms"),(0,_imagehelpers.showElements)(_selectors.default.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(0,_str.getString)("sizecustom_help","tiny_mediacms")},Promise.all([(0,_imagehelpers.bodyImageDetails)(templateContext,this.root),(0,_imagehelpers.footerImageDetails)(templateContext,this.root)]).then((()=>{this.stopImageLoading()})).then((()=>{new _imagedetails.ImageDetails(this.root,this.editor,this.currentModal,this.canShowFilePicker,this.canShowDropZone,url,image).init()})).catch((error=>{window.console.log(error)}))}))}));const permissions=(0,_options.getImagePermissions)(editor),options=(0,_options2.getFilePicker)(editor,"image");this.canShowFilePicker=permissions.filepicker&&void 0!==options&&Object.keys(options.repositories).length>0,this.canShowDropZone=void 0!==options&&Object.values(options.repositories).some((repository=>"upload"===repository.type)),this.editor=editor}async displayDialogue(){const currentImageData=await this.getCurrentImageData();this.currentModal=await _imagemodal.default.create(),this.root=this.currentModal.getRoot()[0],currentImageData&¤tImageData.src?this.loadPreviewImage(currentImageData.src):this.loadInsertImage()}async getTemplateContext(data){return{elementid:this.editor.id,showfilepicker:this.canShowFilePicker,...data}}async getCurrentImageData(){const selectedImageProperties=this.getSelectedImageProperties();if(!selectedImageProperties)return{};const properties={...selectedImageProperties};return properties.src&&(properties.haspreview=!0),properties.alt||(properties.presentation=!0),properties}getSelectedImageProperties(){const image=this.getSelectedImage();if(!image)return this.selectedImage=null,null;const properties={src:null,alt:null,width:null,height:null,presentation:!1,customStyle:""};this.selectedImage=image,properties.customStyle=image.style.cssText;const width=(image=>(0,_imagehelpers.isPercentageValue)(String(image.width))?image.width:parseInt(image.width,10))(image);0!==width&&(properties.width=width);const height=(image=>(0,_imagehelpers.isPercentageValue)(String(image.height))?image.height:parseInt(image.height,10))(image);return 0!==height&&(properties.height=height),properties.src=image.getAttribute("src"),properties.alt=image.getAttribute("alt")||"",properties.presentation="presentation"===image.getAttribute("role"),properties}getSelectedImage(){const imgElm=this.editor.selection.getNode(),figureElm=this.editor.dom.getParent(imgElm,"figure.image");return figureElm?this.editor.dom.select("img",figureElm)[0]:imgElm&&("IMG"!==imgElm.nodeName.toUpperCase()||this.isPlaceholderImage(imgElm))?null:imgElm}isPlaceholderImage(imgElm){return"IMG"===imgElm.nodeName.toUpperCase()&&(imgElm.hasAttribute("data-mce-object")||imgElm.hasAttribute("data-mce-placeholder"))}startImageLoading(){(0,_imagehelpers.showElements)(_selectors.default.IMAGE.elements.loaderIcon,this.root),(0,_imagehelpers.hideElements)(_selectors.default.IMAGE.elements.insertImage,this.root)}stopImageLoading(){(0,_imagehelpers.hideElements)(_selectors.default.IMAGE.elements.loaderIcon,this.root),(0,_imagehelpers.showElements)(_selectors.default.IMAGE.elements.insertImage,this.root)}},_exports.default})); + +//# sourceMappingURL=image.min.js.map \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/image.min.js.map b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/image.min.js.map new file mode 100755 index 00000000..4e8dd218 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/image.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"image.min.js","sources":["../src/image.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 .\n\n/**\n * Tiny Media plugin Image class for Moodle.\n *\n * @module tiny_mediacms/image\n * @copyright 2022 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Selectors from './selectors';\nimport ImageModal from './imagemodal';\nimport {getImagePermissions} from './options';\nimport {getFilePicker} from 'editor_tiny/options';\nimport {ImageInsert} from 'tiny_mediacms/imageinsert';\nimport {ImageDetails} from 'tiny_mediacms/imagedetails';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getString} from 'core/str';\nimport {\n bodyImageInsert,\n footerImageInsert,\n bodyImageDetails,\n footerImageDetails,\n showElements,\n hideElements,\n isPercentageValue,\n} from 'tiny_mediacms/imagehelpers';\n\nprefetchStrings('tiny_mediacms', [\n 'imageurlrequired',\n 'sizecustom_help',\n]);\n\nexport default class MediaImage {\n canShowFilePicker = false;\n editor = null;\n currentModal = null;\n /**\n * @type {HTMLElement|null} The root element.\n */\n root = null;\n\n constructor(editor) {\n const permissions = getImagePermissions(editor);\n const options = getFilePicker(editor, 'image');\n // Indicates whether the file picker can be shown.\n this.canShowFilePicker = permissions.filepicker\n && (typeof options !== 'undefined')\n && Object.keys(options.repositories).length > 0;\n // Indicates whether the drop zone area can be shown.\n this.canShowDropZone = (typeof options !== 'undefined') &&\n Object.values(options.repositories).some(repository => repository.type === 'upload');\n\n this.editor = editor;\n }\n\n async displayDialogue() {\n const currentImageData = await this.getCurrentImageData();\n this.currentModal = await ImageModal.create();\n this.root = this.currentModal.getRoot()[0];\n if (currentImageData && currentImageData.src) {\n this.loadPreviewImage(currentImageData.src);\n } else {\n this.loadInsertImage();\n }\n }\n\n /**\n * Displays an insert image view asynchronously.\n *\n * @returns {Promise}\n */\n loadInsertImage = async function() {\n const templateContext = {\n elementid: this.editor.id,\n showfilepicker: this.canShowFilePicker,\n showdropzone: this.canShowDropZone,\n };\n\n Promise.all([bodyImageInsert(templateContext, this.root), footerImageInsert(templateContext, this.root)])\n .then(() => {\n const imageinsert = new ImageInsert(\n this.root,\n this.editor,\n this.currentModal,\n this.canShowFilePicker,\n this.canShowDropZone,\n );\n imageinsert.init();\n return;\n })\n .catch(error => {\n window.console.log(error);\n });\n };\n\n async getTemplateContext(data) {\n return {\n elementid: this.editor.id,\n showfilepicker: this.canShowFilePicker,\n ...data,\n };\n }\n\n async getCurrentImageData() {\n const selectedImageProperties = this.getSelectedImageProperties();\n if (!selectedImageProperties) {\n return {};\n }\n\n const properties = {...selectedImageProperties};\n\n if (properties.src) {\n properties.haspreview = true;\n }\n\n if (!properties.alt) {\n properties.presentation = true;\n }\n\n return properties;\n }\n\n /**\n * Asynchronously loads and previews an image from the provided URL.\n *\n * @param {string} url - The URL of the image to load and preview.\n * @returns {Promise}\n */\n loadPreviewImage = async function(url) {\n this.startImageLoading();\n const image = new Image();\n image.src = url;\n image.addEventListener('error', async() => {\n const urlWarningLabelEle = this.root.querySelector(Selectors.IMAGE.elements.urlWarning);\n urlWarningLabelEle.innerHTML = await getString('imageurlrequired', 'tiny_mediacms');\n showElements(Selectors.IMAGE.elements.urlWarning, this.root);\n this.stopImageLoading();\n });\n\n image.addEventListener('load', async() => {\n const currentImageData = await this.getCurrentImageData();\n let templateContext = await this.getTemplateContext(currentImageData);\n templateContext.sizecustomhelpicon = {text: await getString('sizecustom_help', 'tiny_mediacms')};\n\n Promise.all([bodyImageDetails(templateContext, this.root), footerImageDetails(templateContext, this.root)])\n .then(() => {\n this.stopImageLoading();\n return;\n })\n .then(() => {\n const imagedetails = new ImageDetails(\n this.root,\n this.editor,\n this.currentModal,\n this.canShowFilePicker,\n this.canShowDropZone,\n url,\n image,\n );\n imagedetails.init();\n return;\n })\n .catch(error => {\n window.console.log(error);\n });\n });\n };\n\n getSelectedImageProperties() {\n const image = this.getSelectedImage();\n if (!image) {\n this.selectedImage = null;\n return null;\n }\n\n const properties = {\n src: null,\n alt: null,\n width: null,\n height: null,\n presentation: false,\n customStyle: '', // Custom CSS styles applied to the image.\n };\n\n const getImageHeight = (image) => {\n if (!isPercentageValue(String(image.height))) {\n return parseInt(image.height, 10);\n }\n\n return image.height;\n };\n\n const getImageWidth = (image) => {\n if (!isPercentageValue(String(image.width))) {\n return parseInt(image.width, 10);\n }\n\n return image.width;\n };\n\n // Get the current selection.\n this.selectedImage = image;\n\n properties.customStyle = image.style.cssText;\n\n const width = getImageWidth(image);\n if (width !== 0) {\n properties.width = width;\n }\n\n const height = getImageHeight(image);\n if (height !== 0) {\n properties.height = height;\n }\n\n properties.src = image.getAttribute('src');\n properties.alt = image.getAttribute('alt') || '';\n properties.presentation = (image.getAttribute('role') === 'presentation');\n\n return properties;\n }\n\n getSelectedImage() {\n const imgElm = this.editor.selection.getNode();\n const figureElm = this.editor.dom.getParent(imgElm, 'figure.image');\n if (figureElm) {\n return this.editor.dom.select('img', figureElm)[0];\n }\n\n if (imgElm && (imgElm.nodeName.toUpperCase() !== 'IMG' || this.isPlaceholderImage(imgElm))) {\n return null;\n }\n return imgElm;\n }\n\n isPlaceholderImage(imgElm) {\n if (imgElm.nodeName.toUpperCase() !== 'IMG') {\n return false;\n }\n\n return (imgElm.hasAttribute('data-mce-object') || imgElm.hasAttribute('data-mce-placeholder'));\n }\n\n /**\n * Displays the upload loader and disables UI elements while loading a file.\n */\n startImageLoading() {\n showElements(Selectors.IMAGE.elements.loaderIcon, this.root);\n hideElements(Selectors.IMAGE.elements.insertImage, this.root);\n }\n\n /**\n * Displays the upload loader and disables UI elements while loading a file.\n */\n stopImageLoading() {\n hideElements(Selectors.IMAGE.elements.loaderIcon, this.root);\n showElements(Selectors.IMAGE.elements.insertImage, this.root);\n }\n}\n"],"names":["constructor","editor","async","templateContext","elementid","this","id","showfilepicker","canShowFilePicker","showdropzone","canShowDropZone","Promise","all","root","then","ImageInsert","currentModal","init","catch","error","window","console","log","url","startImageLoading","image","Image","src","addEventListener","querySelector","Selectors","IMAGE","elements","urlWarning","innerHTML","stopImageLoading","currentImageData","getCurrentImageData","getTemplateContext","sizecustomhelpicon","text","ImageDetails","permissions","options","filepicker","Object","keys","repositories","length","values","some","repository","type","ImageModal","create","getRoot","loadPreviewImage","loadInsertImage","data","selectedImageProperties","getSelectedImageProperties","properties","haspreview","alt","presentation","getSelectedImage","selectedImage","width","height","customStyle","style","cssText","String","parseInt","getImageWidth","getImageHeight","getAttribute","imgElm","selection","getNode","figureElm","dom","getParent","select","nodeName","toUpperCase","isPlaceholderImage","hasAttribute","loaderIcon","insertImage"],"mappings":"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"} \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imagedetails.min.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imagedetails.min.js new file mode 100755 index 00000000..35850139 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imagedetails.min.js @@ -0,0 +1,3 @@ +define("tiny_mediacms/imagedetails",["exports","core/config","core/modal_events","core/notification","core/pending","./selectors","core/templates","core/str","tiny_mediacms/imageinsert","tiny_mediacms/imagehelpers"],(function(_exports,_config,_modal_events,_notification,_pending,_selectors,_templates,_str,_imageinsert,_imagehelpers){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.ImageDetails=void 0,_config=_interopRequireDefault(_config),_modal_events=_interopRequireDefault(_modal_events),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),_selectors=_interopRequireDefault(_selectors),_templates=_interopRequireDefault(_templates);_exports.ImageDetails=class{constructor(root,editor,currentModal,canShowFilePicker,canShowDropZone,currentUrl,_image){_defineProperty(this,"DEFAULTS",{WIDTH:160,HEIGHT:160}),_defineProperty(this,"rawImageDimensions",null),_defineProperty(this,"init",(function(){this.currentModal.setTitle((0,_str.getString)("imagedetails","tiny_mediacms")),this.imageTypeChecked(),this.presentationChanged(),this.storeImageDimensions(this.image),this.setImageDimensions(),this.registerEventListeners()})),_defineProperty(this,"loadInsertImage",(async function(){const templateContext={elementid:this.editor.id,showfilepicker:this.canShowFilePicker,showdropzone:this.canShowDropZone};Promise.all([(0,_imagehelpers.bodyImageInsert)(templateContext,this.root),(0,_imagehelpers.footerImageInsert)(templateContext,this.root)]).then((()=>{new _imageinsert.ImageInsert(this.root,this.editor,this.currentModal,this.canShowFilePicker,this.canShowDropZone).init()})).catch((error=>{window.console.log(error)}))})),_defineProperty(this,"setImageDimensions",(()=>{const imagePreviewBox=this.root.querySelector(_selectors.default.IMAGE.elements.previewBox),image=this.root.querySelector(_selectors.default.IMAGE.elements.preview),widthField=this.root.querySelector(_selectors.default.IMAGE.elements.width),heightField=this.root.querySelector(_selectors.default.IMAGE.elements.height),updateImageDimensions=()=>{const boxWidth=imagePreviewBox.clientWidth,boxHeight=imagePreviewBox.clientHeight,dimensions=this.fitSquareIntoBox(widthField.value,heightField.value,boxWidth,boxHeight);image.style.width="".concat(dimensions.width,"px"),image.style.height="".concat(dimensions.height,"px")};0===imagePreviewBox.clientWidth?this.currentModal.getRoot().on(_modal_events.default.shown,(()=>{updateImageDimensions()})):updateImageDimensions()})),_defineProperty(this,"fitSquareIntoBox",((squareWidth,squareHeight,boxWidth,boxHeight)=>{if(squareWidth(""===element.value&&(element.value=this.rawImageDimensions.width),element.value))(this.root.querySelector(_selectors.default.IMAGE.elements.width)),currentHeight=(element=>(""===element.value&&(element.value=this.rawImageDimensions.height),element.value))(this.root.querySelector(_selectors.default.IMAGE.elements.height)),preview=this.root.querySelector(_selectors.default.IMAGE.elements.preview);preview.setAttribute("src",image.src),preview.style.display="";const constrain=this.root.querySelector(_selectors.default.IMAGE.elements.constrain);if((0,_imagehelpers.isPercentageValue)(currentWidth)&&(0,_imagehelpers.isPercentageValue)(currentHeight))constrain.checked=currentWidth===currentHeight;else if(0===image.width||0===image.height)constrain.disabled="disabled";else{const widthRatio=Math.round(100*parseInt(currentWidth,10)/image.width),heightRatio=Math.round(100*parseInt(currentHeight,10)/image.height);constrain.checked=widthRatio===heightRatio}((currentWidth,currentHeight)=>{this.rawImageDimensions.width===currentWidth&&this.rawImageDimensions.height===currentHeight?(this.currentWidth=this.rawImageDimensions.width,this.currentHeight=this.rawImageDimensions.height,this.sizeChecked("original")):(this.currentWidth=currentWidth,this.currentHeight=currentHeight,this.sizeChecked("custom"))})(Number(currentWidth),Number(currentHeight))}sizeChecked(option){const widthInput=this.root.querySelector(_selectors.default.IMAGE.elements.width),heightInput=this.root.querySelector(_selectors.default.IMAGE.elements.height);if("original"===option)this.sizeOriginalChecked(),widthInput.value=this.rawImageDimensions.width,heightInput.value=this.rawImageDimensions.height;else if("custom"===option&&(this.sizeCustomChecked(),widthInput.value=this.currentWidth,heightInput.value=this.currentHeight,this.currentWidth===this.rawImageDimensions.width&&this.currentHeight===this.rawImageDimensions.height)){this.root.querySelector(_selectors.default.IMAGE.elements.constrain).checked=!0}this.autoAdjustSize()}autoAdjustSize(){let forceHeight=arguments.length>0&&void 0!==arguments[0]&&arguments[0];if(!this.rawImageDimensions)return;const widthField=this.root.querySelector(_selectors.default.IMAGE.elements.width),heightField=this.root.querySelector(_selectors.default.IMAGE.elements.height),normalizeFieldData=fieldData=>(fieldData.isPercentageValue=!!(0,_imagehelpers.isPercentageValue)(fieldData.field.value),fieldData.isPercentageValue?(fieldData.percentValue=parseInt(fieldData.field.value,10),fieldData.pixelSize=this.rawImageDimensions[fieldData.type]/100*fieldData.percentValue):(fieldData.pixelSize=parseInt(fieldData.field.value,10),fieldData.percentValue=fieldData.pixelSize/this.rawImageDimensions[fieldData.type]*100),fieldData),getKeyField=()=>{const currentValue=forceHeight?{field:heightField,type:"height"}:{field:widthField,type:"width"};return""===currentValue.field.value&&(currentValue.field.value=this.rawImageDimensions[currentValue.type]),normalizeFieldData(currentValue)};if(this.root.querySelector(_selectors.default.IMAGE.elements.constrain).checked){const keyField=getKeyField(),relativeField=normalizeFieldData(forceHeight?{field:widthField,type:"width"}:{field:heightField,type:"height"});keyField.isPercentageValue?(relativeField.field.value=keyField.field.value,relativeField.percentValue=keyField.percentValue):(relativeField.pixelSize=Math.round(keyField.pixelSize/this.rawImageDimensions[keyField.type]*this.rawImageDimensions[relativeField.type]),relativeField.field.value=relativeField.pixelSize)}this.currentWidth=Number(widthField.value)!==this.rawImageDimensions.width?widthField.value:this.currentWidth,this.currentHeight=Number(heightField.value)!==this.rawImageDimensions.height?heightField.value:this.currentHeight}sizeOriginalChecked(){this.root.querySelector(_selectors.default.IMAGE.elements.sizeOriginal).checked=!0,this.root.querySelector(_selectors.default.IMAGE.elements.sizeCustom).checked=!1,(0,_imagehelpers.hideElements)(_selectors.default.IMAGE.elements.properties,this.root)}sizeCustomChecked(){this.root.querySelector(_selectors.default.IMAGE.elements.sizeOriginal).checked=!1,this.root.querySelector(_selectors.default.IMAGE.elements.sizeCustom).checked=!0,(0,_imagehelpers.showElements)(_selectors.default.IMAGE.elements.properties,this.root)}presentationChanged(){const presentation=this.root.querySelector(_selectors.default.IMAGE.elements.presentation);this.root.querySelector(_selectors.default.IMAGE.elements.alt).disabled=presentation.checked,this.handleKeyupCharacterCount()}imageTypeChecked(){const isExternalUrl=!1===new RegExp("".concat(_config.default.wwwroot)).test(this.currentUrl);if((0,_imagehelpers.hideElements)(_selectors.default.IMAGE.elements.url,this.root),isExternalUrl)this.setFilenameLabel(decodeURI(this.currentUrl));else{const filename=this.currentUrl.split("/").pop().split("?")[0];this.setFilenameLabel(decodeURI(filename))}}setFilenameLabel(label){const urlLabelEle=this.root.querySelector(_selectors.default.IMAGE.elements.fileNameLabel);urlLabelEle&&(urlLabelEle.innerHTML=label,urlLabelEle.setAttribute("title",label))}toggleAriaInvalid(selectors,predicate){selectors.forEach((selector=>{this.root.querySelectorAll(selector).forEach((element=>element.setAttribute("aria-invalid",predicate)))}))}hasErrorUrlField(){const urlError=""===this.currentUrl;return urlError?(0,_imagehelpers.showElements)(_selectors.default.IMAGE.elements.urlWarning,this.root):(0,_imagehelpers.hideElements)(_selectors.default.IMAGE.elements.urlWarning,this.root),this.toggleAriaInvalid([_selectors.default.IMAGE.elements.url],urlError),urlError}hasErrorAltField(){const alt=this.root.querySelector(_selectors.default.IMAGE.elements.alt).value,presentation=this.root.querySelector(_selectors.default.IMAGE.elements.presentation).checked,imageAltError=""===alt&&!presentation;return imageAltError?(0,_imagehelpers.showElements)(_selectors.default.IMAGE.elements.altWarning,this.root):(0,_imagehelpers.hideElements)(_selectors.default.IMAGE.elements.urlWaaltWarningrning,this.root),this.toggleAriaInvalid([_selectors.default.IMAGE.elements.alt,_selectors.default.IMAGE.elements.presentation],imageAltError),imageAltError}updateWarning(){const urlError=this.hasErrorUrlField(),imageAltError=this.hasErrorAltField();return urlError||imageAltError}getImageContext(){if(this.updateWarning())return null;const classList=[],constrain=this.root.querySelector(_selectors.default.IMAGE.elements.constrain).checked,sizeOriginal=this.root.querySelector(_selectors.default.IMAGE.elements.sizeOriginal).checked;return constrain||sizeOriginal?classList.push(_selectors.default.IMAGE.styles.responsive):classList.pop(_selectors.default.IMAGE.styles.responsive),{url:this.currentUrl,alt:this.root.querySelector(_selectors.default.IMAGE.elements.alt).value,width:this.root.querySelector(_selectors.default.IMAGE.elements.width).value,height:this.root.querySelector(_selectors.default.IMAGE.elements.height).value,presentation:this.root.querySelector(_selectors.default.IMAGE.elements.presentation).checked,customStyle:this.root.querySelector(_selectors.default.IMAGE.elements.customStyle).value,classlist:classList.join(" ")}}setImage(){const pendingPromise=new _pending.default("tiny_mediacms:setImage");if(""===this.currentUrl)return;if(this.updateWarning())return void pendingPromise.resolve();const width=this.root.querySelector(_selectors.default.IMAGE.elements.width).value;if(!(0,_imagehelpers.isPercentageValue)(width)&&isNaN(parseInt(width,10)))return this.root.querySelector(_selectors.default.IMAGE.elements.width).focus(),void pendingPromise.resolve();const height=this.root.querySelector(_selectors.default.IMAGE.elements.height).value;if(!(0,_imagehelpers.isPercentageValue)(height)&&isNaN(parseInt(height,10)))return this.root.querySelector(_selectors.default.IMAGE.elements.height).focus(),void pendingPromise.resolve();_templates.default.render("tiny_mediacms/image",this.getImageContext()).then((html=>(this.editor.insertContent(html),this.currentModal.destroy(),pendingPromise.resolve(),html))).catch((error=>{window.console.log(error)}))}deleteImage(){_notification.default.deleteCancelPromise((0,_str.getString)("deleteimage","tiny_mediacms"),(0,_str.getString)("deleteimagewarning","tiny_mediacms")).then((()=>{(0,_imagehelpers.hideElements)(_selectors.default.IMAGE.elements.altWarning,this.root),this.loadInsertImage()})).catch((error=>{window.console.log(error)}))}registerEventListeners(){this.root.querySelector(_selectors.default.IMAGE.actions.submit).addEventListener("click",(e=>{e.preventDefault(),this.setImage()}));const deleteImageEle=this.root.querySelector(_selectors.default.IMAGE.actions.deleteImage);deleteImageEle.addEventListener("click",(()=>{this.deleteImage()})),deleteImageEle.addEventListener("keydown",(e=>{"Enter"===e.key&&this.deleteImage()})),this.root.addEventListener("change",(e=>{e.target.closest(_selectors.default.IMAGE.elements.presentation)&&this.presentationChanged();e.target.closest(_selectors.default.IMAGE.elements.constrain)&&this.autoAdjustSize();e.target.closest(_selectors.default.IMAGE.elements.sizeOriginal)&&this.sizeChecked("original");e.target.closest(_selectors.default.IMAGE.elements.sizeCustom)&&this.sizeChecked("custom")})),this.root.addEventListener("blur",(e=>{if(e.target.nodeType===Node.ELEMENT_NODE){e.target.closest(_selectors.default.IMAGE.elements.presentation)&&this.presentationChanged()}}),!0),this.root.addEventListener("keyup",(e=>{e.target.closest(_selectors.default.IMAGE.elements.alt)&&this.handleKeyupCharacterCount()})),this.root.addEventListener("input",(e=>{const widthEle=e.target.closest(_selectors.default.IMAGE.elements.width);widthEle&&(widthEle.value=""===widthEle.value?0:Number(widthEle.value),this.autoAdjustSize());const heightEle=e.target.closest(_selectors.default.IMAGE.elements.height);heightEle&&(heightEle.value=""===heightEle.value?0:Number(heightEle.value),this.autoAdjustSize(!0))}))}handleKeyupCharacterCount(){const alt=this.root.querySelector(_selectors.default.IMAGE.elements.alt).value;this.root.querySelector("#currentcount").innerHTML=alt.length}}})); + +//# sourceMappingURL=imagedetails.min.js.map \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imagedetails.min.js.map b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imagedetails.min.js.map new file mode 100755 index 00000000..0d7ebb6c --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imagedetails.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"imagedetails.min.js","sources":["../src/imagedetails.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 .\n\n/**\n * Tiny media plugin image details class for Moodle.\n *\n * @module tiny_mediacms/imagedetails\n * @copyright 2024 Meirza \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Config from 'core/config';\nimport ModalEvents from 'core/modal_events';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport Selectors from './selectors';\nimport Templates from 'core/templates';\nimport {getString} from 'core/str';\nimport {ImageInsert} from 'tiny_mediacms/imageinsert';\nimport {\n bodyImageInsert,\n footerImageInsert,\n showElements,\n hideElements,\n isPercentageValue,\n} from 'tiny_mediacms/imagehelpers';\n\nexport class ImageDetails {\n DEFAULTS = {\n WIDTH: 160,\n HEIGHT: 160,\n };\n\n rawImageDimensions = null;\n\n constructor(\n root,\n editor,\n currentModal,\n canShowFilePicker,\n canShowDropZone,\n currentUrl,\n image,\n ) {\n this.root = root;\n this.editor = editor;\n this.currentModal = currentModal;\n this.canShowFilePicker = canShowFilePicker;\n this.canShowDropZone = canShowDropZone;\n this.currentUrl = currentUrl;\n this.image = image;\n }\n\n init = function() {\n this.currentModal.setTitle(getString('imagedetails', 'tiny_mediacms'));\n this.imageTypeChecked();\n this.presentationChanged();\n this.storeImageDimensions(this.image);\n this.setImageDimensions();\n this.registerEventListeners();\n };\n\n /**\n * Loads and displays a preview image based on the provided URL, and handles image loading events.\n */\n loadInsertImage = async function() {\n const templateContext = {\n elementid: this.editor.id,\n showfilepicker: this.canShowFilePicker,\n showdropzone: this.canShowDropZone,\n };\n\n Promise.all([bodyImageInsert(templateContext, this.root), footerImageInsert(templateContext, this.root)])\n .then(() => {\n const imageinsert = new ImageInsert(\n this.root,\n this.editor,\n this.currentModal,\n this.canShowFilePicker,\n this.canShowDropZone,\n );\n imageinsert.init();\n return;\n })\n .catch(error => {\n window.console.log(error);\n });\n };\n\n storeImageDimensions(image) {\n // Store dimensions of the raw image, falling back to defaults for images without dimensions (e.g. SVG).\n this.rawImageDimensions = {\n width: image.width || this.DEFAULTS.WIDTH,\n height: image.height || this.DEFAULTS.HEIGHT,\n };\n\n const getCurrentWidth = (element) => {\n if (element.value === '') {\n element.value = this.rawImageDimensions.width;\n }\n return element.value;\n };\n\n const getCurrentHeight = (element) => {\n if (element.value === '') {\n element.value = this.rawImageDimensions.height;\n }\n return element.value;\n };\n\n const widthInput = this.root.querySelector(Selectors.IMAGE.elements.width);\n const currentWidth = getCurrentWidth(widthInput);\n\n const heightInput = this.root.querySelector(Selectors.IMAGE.elements.height);\n const currentHeight = getCurrentHeight(heightInput);\n\n const preview = this.root.querySelector(Selectors.IMAGE.elements.preview);\n preview.setAttribute('src', image.src);\n preview.style.display = '';\n\n // Ensure the checkbox always in unchecked status when an image loads at first.\n const constrain = this.root.querySelector(Selectors.IMAGE.elements.constrain);\n if (isPercentageValue(currentWidth) && isPercentageValue(currentHeight)) {\n constrain.checked = currentWidth === currentHeight;\n } else if (image.width === 0 || image.height === 0) {\n // If we don't have both dimensions of the image, we can't auto-size it, so disable control.\n constrain.disabled = 'disabled';\n } else {\n // This is the same as comparing to 3 decimal places.\n const widthRatio = Math.round(100 * parseInt(currentWidth, 10) / image.width);\n const heightRatio = Math.round(100 * parseInt(currentHeight, 10) / image.height);\n constrain.checked = widthRatio === heightRatio;\n }\n\n /**\n * Sets the selected size option based on current width and height values.\n *\n * @param {number} currentWidth - The current width value.\n * @param {number} currentHeight - The current height value.\n */\n const setSelectedSize = (currentWidth, currentHeight) => {\n if (this.rawImageDimensions.width === currentWidth &&\n this.rawImageDimensions.height === currentHeight\n ) {\n this.currentWidth = this.rawImageDimensions.width;\n this.currentHeight = this.rawImageDimensions.height;\n this.sizeChecked('original');\n } else {\n this.currentWidth = currentWidth;\n this.currentHeight = currentHeight;\n this.sizeChecked('custom');\n }\n };\n\n setSelectedSize(Number(currentWidth), Number(currentHeight));\n }\n\n /**\n * Handles the selection of image size options and updates the form inputs accordingly.\n *\n * @param {string} option - The selected image size option (\"original\" or \"custom\").\n */\n sizeChecked(option) {\n const widthInput = this.root.querySelector(Selectors.IMAGE.elements.width);\n const heightInput = this.root.querySelector(Selectors.IMAGE.elements.height);\n if (option === \"original\") {\n this.sizeOriginalChecked();\n widthInput.value = this.rawImageDimensions.width;\n heightInput.value = this.rawImageDimensions.height;\n } else if (option === \"custom\") {\n this.sizeCustomChecked();\n widthInput.value = this.currentWidth;\n heightInput.value = this.currentHeight;\n\n // If the current size is equal to the original size, then check the Keep proportion checkbox.\n if (this.currentWidth === this.rawImageDimensions.width && this.currentHeight === this.rawImageDimensions.height) {\n const constrainField = this.root.querySelector(Selectors.IMAGE.elements.constrain);\n constrainField.checked = true;\n }\n }\n this.autoAdjustSize();\n }\n\n autoAdjustSize(forceHeight = false) {\n // If we do not know the image size, do not do anything.\n if (!this.rawImageDimensions) {\n return;\n }\n\n const widthField = this.root.querySelector(Selectors.IMAGE.elements.width);\n const heightField = this.root.querySelector(Selectors.IMAGE.elements.height);\n\n const normalizeFieldData = (fieldData) => {\n fieldData.isPercentageValue = !!isPercentageValue(fieldData.field.value);\n if (fieldData.isPercentageValue) {\n fieldData.percentValue = parseInt(fieldData.field.value, 10);\n fieldData.pixelSize = this.rawImageDimensions[fieldData.type] / 100 * fieldData.percentValue;\n } else {\n fieldData.pixelSize = parseInt(fieldData.field.value, 10);\n fieldData.percentValue = fieldData.pixelSize / this.rawImageDimensions[fieldData.type] * 100;\n }\n\n return fieldData;\n };\n\n const getKeyField = () => {\n const getValue = () => {\n if (forceHeight) {\n return {\n field: heightField,\n type: 'height',\n };\n } else {\n return {\n field: widthField,\n type: 'width',\n };\n }\n };\n\n const currentValue = getValue();\n if (currentValue.field.value === '') {\n currentValue.field.value = this.rawImageDimensions[currentValue.type];\n }\n\n return normalizeFieldData(currentValue);\n };\n\n const getRelativeField = () => {\n if (forceHeight) {\n return normalizeFieldData({\n field: widthField,\n type: 'width',\n });\n } else {\n return normalizeFieldData({\n field: heightField,\n type: 'height',\n });\n }\n };\n\n // Now update with the new values.\n const constrainField = this.root.querySelector(Selectors.IMAGE.elements.constrain);\n if (constrainField.checked) {\n const keyField = getKeyField();\n const relativeField = getRelativeField();\n // We are keeping the image in proportion.\n // Calculate the size for the relative field.\n if (keyField.isPercentageValue) {\n // In proportion, so the percentages are the same.\n relativeField.field.value = keyField.field.value;\n relativeField.percentValue = keyField.percentValue;\n } else {\n relativeField.pixelSize = Math.round(\n keyField.pixelSize / this.rawImageDimensions[keyField.type] * this.rawImageDimensions[relativeField.type]\n );\n relativeField.field.value = relativeField.pixelSize;\n }\n }\n\n // Store the custom width and height to reuse.\n this.currentWidth = Number(widthField.value) !== this.rawImageDimensions.width ? widthField.value : this.currentWidth;\n this.currentHeight = Number(heightField.value) !== this.rawImageDimensions.height ? heightField.value : this.currentHeight;\n }\n\n /**\n * Sets the dimensions of the image preview element based on user input and constraints.\n */\n setImageDimensions = () => {\n const imagePreviewBox = this.root.querySelector(Selectors.IMAGE.elements.previewBox);\n const image = this.root.querySelector(Selectors.IMAGE.elements.preview);\n const widthField = this.root.querySelector(Selectors.IMAGE.elements.width);\n const heightField = this.root.querySelector(Selectors.IMAGE.elements.height);\n\n const updateImageDimensions = () => {\n // Get the latest dimensions of the preview box for responsiveness.\n const boxWidth = imagePreviewBox.clientWidth;\n const boxHeight = imagePreviewBox.clientHeight;\n // Get the new width and height for the image.\n const dimensions = this.fitSquareIntoBox(widthField.value, heightField.value, boxWidth, boxHeight);\n image.style.width = `${dimensions.width}px`;\n image.style.height = `${dimensions.height}px`;\n };\n // If the client size is zero, then get the new dimensions once the modal is shown.\n if (imagePreviewBox.clientWidth === 0) {\n // Call the shown event.\n this.currentModal.getRoot().on(ModalEvents.shown, () => {\n updateImageDimensions();\n });\n } else {\n updateImageDimensions();\n }\n };\n\n /**\n * Handles the selection of the \"Original Size\" option and updates the form elements accordingly.\n */\n sizeOriginalChecked() {\n this.root.querySelector(Selectors.IMAGE.elements.sizeOriginal).checked = true;\n this.root.querySelector(Selectors.IMAGE.elements.sizeCustom).checked = false;\n hideElements(Selectors.IMAGE.elements.properties, this.root);\n }\n\n /**\n * Handles the selection of the \"Custom Size\" option and updates the form elements accordingly.\n */\n sizeCustomChecked() {\n this.root.querySelector(Selectors.IMAGE.elements.sizeOriginal).checked = false;\n this.root.querySelector(Selectors.IMAGE.elements.sizeCustom).checked = true;\n showElements(Selectors.IMAGE.elements.properties, this.root);\n }\n\n /**\n * Handles changes in the image presentation checkbox and enables/disables the image alt text input accordingly.\n */\n presentationChanged() {\n const presentation = this.root.querySelector(Selectors.IMAGE.elements.presentation);\n const alt = this.root.querySelector(Selectors.IMAGE.elements.alt);\n alt.disabled = presentation.checked;\n\n // Counting the image description characters.\n this.handleKeyupCharacterCount();\n }\n\n /**\n * This function checks whether an image URL is local (within the same website's domain) or external (from an external source).\n * Depending on the result, it dynamically updates the visibility and content of HTML elements in a user interface.\n * If the image is local then we only show it's filename.\n * If the image is external then it will show full URL and it can be updated.\n */\n imageTypeChecked() {\n const regex = new RegExp(`${Config.wwwroot}`);\n\n // True if the URL is from external, otherwise false.\n const isExternalUrl = regex.test(this.currentUrl) === false;\n\n // Hide the URL input.\n hideElements(Selectors.IMAGE.elements.url, this.root);\n\n if (!isExternalUrl) {\n // Split the URL by '/' to get an array of segments.\n const segments = this.currentUrl.split('/');\n // Get the last segment, which should be the filename.\n const filename = segments.pop().split('?')[0];\n // Show the file name.\n this.setFilenameLabel(decodeURI(filename));\n } else {\n\n this.setFilenameLabel(decodeURI(this.currentUrl));\n }\n }\n\n /**\n * Set the string for the URL label element.\n *\n * @param {string} label - The label text to set.\n */\n setFilenameLabel(label) {\n const urlLabelEle = this.root.querySelector(Selectors.IMAGE.elements.fileNameLabel);\n if (urlLabelEle) {\n urlLabelEle.innerHTML = label;\n urlLabelEle.setAttribute(\"title\", label);\n }\n }\n\n toggleAriaInvalid(selectors, predicate) {\n selectors.forEach((selector) => {\n const elements = this.root.querySelectorAll(selector);\n elements.forEach((element) => element.setAttribute('aria-invalid', predicate));\n });\n }\n\n hasErrorUrlField() {\n const urlError = this.currentUrl === '';\n if (urlError) {\n showElements(Selectors.IMAGE.elements.urlWarning, this.root);\n } else {\n hideElements(Selectors.IMAGE.elements.urlWarning, this.root);\n }\n this.toggleAriaInvalid([Selectors.IMAGE.elements.url], urlError);\n\n return urlError;\n }\n\n hasErrorAltField() {\n const alt = this.root.querySelector(Selectors.IMAGE.elements.alt).value;\n const presentation = this.root.querySelector(Selectors.IMAGE.elements.presentation).checked;\n const imageAltError = alt === '' && !presentation;\n if (imageAltError) {\n showElements(Selectors.IMAGE.elements.altWarning, this.root);\n } else {\n hideElements(Selectors.IMAGE.elements.urlWaaltWarningrning, this.root);\n }\n this.toggleAriaInvalid([Selectors.IMAGE.elements.alt, Selectors.IMAGE.elements.presentation], imageAltError);\n\n return imageAltError;\n }\n\n updateWarning() {\n const urlError = this.hasErrorUrlField();\n const imageAltError = this.hasErrorAltField();\n\n return urlError || imageAltError;\n }\n\n getImageContext() {\n // Check if there are any accessibility issues.\n if (this.updateWarning()) {\n return null;\n }\n\n const classList = [];\n const constrain = this.root.querySelector(Selectors.IMAGE.elements.constrain).checked;\n const sizeOriginal = this.root.querySelector(Selectors.IMAGE.elements.sizeOriginal).checked;\n if (constrain || sizeOriginal) {\n // If the Auto size checkbox is checked or the Original size is checked, then apply the responsive class.\n classList.push(Selectors.IMAGE.styles.responsive);\n } else {\n // Otherwise, remove it.\n classList.pop(Selectors.IMAGE.styles.responsive);\n }\n\n return {\n url: this.currentUrl,\n alt: this.root.querySelector(Selectors.IMAGE.elements.alt).value,\n width: this.root.querySelector(Selectors.IMAGE.elements.width).value,\n height: this.root.querySelector(Selectors.IMAGE.elements.height).value,\n presentation: this.root.querySelector(Selectors.IMAGE.elements.presentation).checked,\n customStyle: this.root.querySelector(Selectors.IMAGE.elements.customStyle).value,\n classlist: classList.join(' '),\n };\n }\n\n setImage() {\n const pendingPromise = new Pending('tiny_mediacms:setImage');\n const url = this.currentUrl;\n if (url === '') {\n return;\n }\n\n // Check if there are any accessibility issues.\n if (this.updateWarning()) {\n pendingPromise.resolve();\n return;\n }\n\n // Check for invalid width or height.\n const width = this.root.querySelector(Selectors.IMAGE.elements.width).value;\n if (!isPercentageValue(width) && isNaN(parseInt(width, 10))) {\n this.root.querySelector(Selectors.IMAGE.elements.width).focus();\n pendingPromise.resolve();\n return;\n }\n\n const height = this.root.querySelector(Selectors.IMAGE.elements.height).value;\n if (!isPercentageValue(height) && isNaN(parseInt(height, 10))) {\n this.root.querySelector(Selectors.IMAGE.elements.height).focus();\n pendingPromise.resolve();\n return;\n }\n\n Templates.render('tiny_mediacms/image', this.getImageContext())\n .then((html) => {\n this.editor.insertContent(html);\n this.currentModal.destroy();\n pendingPromise.resolve();\n\n return html;\n })\n .catch(error => {\n window.console.log(error);\n });\n }\n\n /**\n * Deletes the image after confirming with the user and loads the insert image page.\n */\n deleteImage() {\n Notification.deleteCancelPromise(\n getString('deleteimage', 'tiny_mediacms'),\n getString('deleteimagewarning', 'tiny_mediacms'),\n ).then(() => {\n hideElements(Selectors.IMAGE.elements.altWarning, this.root);\n // Removing the image in the preview will bring the user to the insert page.\n this.loadInsertImage();\n return;\n }).catch(error => {\n window.console.log(error);\n });\n }\n\n registerEventListeners() {\n const submitAction = this.root.querySelector(Selectors.IMAGE.actions.submit);\n submitAction.addEventListener('click', (e) => {\n e.preventDefault();\n this.setImage();\n });\n\n const deleteImageEle = this.root.querySelector(Selectors.IMAGE.actions.deleteImage);\n deleteImageEle.addEventListener('click', () => {\n this.deleteImage();\n });\n deleteImageEle.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\") {\n this.deleteImage();\n }\n });\n\n this.root.addEventListener('change', (e) => {\n const presentationEle = e.target.closest(Selectors.IMAGE.elements.presentation);\n if (presentationEle) {\n this.presentationChanged();\n }\n\n const constrainEle = e.target.closest(Selectors.IMAGE.elements.constrain);\n if (constrainEle) {\n this.autoAdjustSize();\n }\n\n const sizeOriginalEle = e.target.closest(Selectors.IMAGE.elements.sizeOriginal);\n if (sizeOriginalEle) {\n this.sizeChecked('original');\n }\n\n const sizeCustomEle = e.target.closest(Selectors.IMAGE.elements.sizeCustom);\n if (sizeCustomEle) {\n this.sizeChecked('custom');\n }\n });\n\n this.root.addEventListener('blur', (e) => {\n if (e.target.nodeType === Node.ELEMENT_NODE) {\n\n const presentationEle = e.target.closest(Selectors.IMAGE.elements.presentation);\n if (presentationEle) {\n this.presentationChanged();\n }\n }\n }, true);\n\n // Character count.\n this.root.addEventListener('keyup', (e) => {\n const altEle = e.target.closest(Selectors.IMAGE.elements.alt);\n if (altEle) {\n this.handleKeyupCharacterCount();\n }\n });\n\n this.root.addEventListener('input', (e) => {\n const widthEle = e.target.closest(Selectors.IMAGE.elements.width);\n if (widthEle) {\n // Avoid empty value.\n widthEle.value = widthEle.value === \"\" ? 0 : Number(widthEle.value);\n this.autoAdjustSize();\n }\n\n const heightEle = e.target.closest(Selectors.IMAGE.elements.height);\n if (heightEle) {\n // Avoid empty value.\n heightEle.value = heightEle.value === \"\" ? 0 : Number(heightEle.value);\n this.autoAdjustSize(true);\n }\n });\n }\n\n handleKeyupCharacterCount() {\n const alt = this.root.querySelector(Selectors.IMAGE.elements.alt).value;\n const current = this.root.querySelector('#currentcount');\n current.innerHTML = alt.length;\n }\n\n /**\n * Calculates the dimensions to fit a square into a specified box while maintaining aspect ratio.\n *\n * @param {number} squareWidth - The width of the square.\n * @param {number} squareHeight - The height of the square.\n * @param {number} boxWidth - The width of the box.\n * @param {number} boxHeight - The height of the box.\n * @returns {Object} An object with the new width and height of the square to fit in the box.\n */\n fitSquareIntoBox = (squareWidth, squareHeight, boxWidth, boxHeight) => {\n if (squareWidth < boxWidth && squareHeight < boxHeight) {\n // If the square is smaller than the box, keep its dimensions.\n return {\n width: squareWidth,\n height: squareHeight,\n };\n }\n // Calculate the scaling factor based on the minimum scaling required to fit in the box.\n const widthScaleFactor = boxWidth / squareWidth;\n const heightScaleFactor = boxHeight / squareHeight;\n const minScaleFactor = Math.min(widthScaleFactor, heightScaleFactor);\n // Scale the square's dimensions based on the aspect ratio and the minimum scaling factor.\n const newWidth = squareWidth * minScaleFactor;\n const newHeight = squareHeight * minScaleFactor;\n return {\n width: newWidth,\n height: newHeight,\n };\n };\n}\n"],"names":["constructor","root","editor","currentModal","canShowFilePicker","canShowDropZone","currentUrl","image","WIDTH","HEIGHT","setTitle","imageTypeChecked","presentationChanged","storeImageDimensions","this","setImageDimensions","registerEventListeners","async","templateContext","elementid","id","showfilepicker","showdropzone","Promise","all","then","ImageInsert","init","catch","error","window","console","log","imagePreviewBox","querySelector","Selectors","IMAGE","elements","previewBox","preview","widthField","width","heightField","height","updateImageDimensions","boxWidth","clientWidth","boxHeight","clientHeight","dimensions","fitSquareIntoBox","value","style","getRoot","on","ModalEvents","shown","squareWidth","squareHeight","widthScaleFactor","heightScaleFactor","minScaleFactor","Math","min","rawImageDimensions","DEFAULTS","currentWidth","element","getCurrentWidth","currentHeight","getCurrentHeight","setAttribute","src","display","constrain","checked","disabled","widthRatio","round","parseInt","heightRatio","sizeChecked","setSelectedSize","Number","option","widthInput","heightInput","sizeOriginalChecked","sizeCustomChecked","autoAdjustSize","forceHeight","normalizeFieldData","fieldData","isPercentageValue","field","percentValue","pixelSize","type","getKeyField","currentValue","keyField","relativeField","sizeOriginal","sizeCustom","properties","presentation","alt","handleKeyupCharacterCount","isExternalUrl","RegExp","Config","wwwroot","test","url","setFilenameLabel","decodeURI","filename","split","pop","label","urlLabelEle","fileNameLabel","innerHTML","toggleAriaInvalid","selectors","predicate","forEach","selector","querySelectorAll","hasErrorUrlField","urlError","urlWarning","hasErrorAltField","imageAltError","altWarning","urlWaaltWarningrning","updateWarning","getImageContext","classList","push","styles","responsive","customStyle","classlist","join","setImage","pendingPromise","Pending","resolve","isNaN","focus","render","html","insertContent","destroy","deleteImage","deleteCancelPromise","loadInsertImage","actions","submit","addEventListener","e","preventDefault","deleteImageEle","key","target","closest","nodeType","Node","ELEMENT_NODE","widthEle","heightEle","length"],"mappings":"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"} \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imagehelpers.min.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imagehelpers.min.js new file mode 100755 index 00000000..60b5e2de --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imagehelpers.min.js @@ -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 + * @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 \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imagehelpers.min.js.map b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imagehelpers.min.js.map new file mode 100755 index 00000000..d6b87c56 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imagehelpers.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"imagehelpers.min.js","sources":["../src/imagehelpers.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 .\n\n/**\n * Tiny media plugin image helpers.\n *\n * @module tiny_mediacms/imagehelpers\n * @copyright 2024 Meirza \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\n\n/**\n * Renders and inserts the body template for inserting an image into the modal.\n *\n * @param {object} templateContext - The context for rendering the template.\n * @param {HTMLElement} root - The root element where the template will be inserted.\n * @returns {Promise}\n */\nexport const bodyImageInsert = async(templateContext, root) => {\n return Templates.renderForPromise('tiny_mediacms/insert_image_modal_insert', {...templateContext})\n .then(({html, js}) => {\n Templates.replaceNodeContents(root.querySelector('.tiny_imagecms_body_template'), html, js);\n return;\n })\n .catch(error => {\n window.console.log(error);\n });\n};\n\n/**\n * Renders and inserts the footer template for inserting an image into the modal.\n *\n * @param {object} templateContext - The context for rendering the template.\n * @param {HTMLElement} root - The root element where the template will be inserted.\n * @returns {Promise}\n */\nexport const footerImageInsert = async(templateContext, root) => {\n return Templates.renderForPromise('tiny_mediacms/insert_image_modal_insert_footer', {...templateContext})\n .then(({html, js}) => {\n Templates.replaceNodeContents(root.querySelector('.tiny_imagecms_footer_template'), html, js);\n return;\n })\n .catch(error => {\n window.console.log(error);\n });\n};\n\n/**\n * Renders and inserts the body template for displaying image details in the modal.\n *\n * @param {object} templateContext - The context for rendering the template.\n * @param {HTMLElement} root - The root element where the template will be inserted.\n * @returns {Promise}\n */\nexport const bodyImageDetails = async(templateContext, root) => {\n return Templates.renderForPromise('tiny_mediacms/insert_image_modal_details', {...templateContext})\n .then(({html, js}) => {\n Templates.replaceNodeContents(root.querySelector('.tiny_imagecms_body_template'), html, js);\n return;\n })\n .catch(error => {\n window.console.log(error);\n });\n};\n\n/**\n * Renders and inserts the footer template for displaying image details in the modal.\n * @param {object} templateContext - The context for rendering the template.\n * @param {HTMLElement} root - The root element where the template will be inserted.\n * @returns {Promise}\n */\nexport const footerImageDetails = async(templateContext, root) => {\n return Templates.renderForPromise('tiny_mediacms/insert_image_modal_details_footer', {...templateContext})\n .then(({html, js}) => {\n Templates.replaceNodeContents(root.querySelector('.tiny_imagecms_footer_template'), html, js);\n return;\n })\n .catch(error => {\n window.console.log(error);\n });\n};\n\n/**\n * Show the element(s).\n *\n * @param {string|string[]} elements - The CSS selector for the elements to toggle.\n * @param {object} root - The CSS selector for the elements to toggle.\n */\nexport const showElements = (elements, root) => {\n if (elements instanceof Array) {\n elements.forEach((elementSelector) => {\n const element = root.querySelector(elementSelector);\n if (element) {\n element.classList.remove('d-none');\n }\n });\n } else {\n const element = root.querySelector(elements);\n if (element) {\n element.classList.remove('d-none');\n }\n }\n};\n\n/**\n * Hide the element(s).\n *\n * @param {string|string[]} elements - The CSS selector for the elements to toggle.\n * @param {object} root - The CSS selector for the elements to toggle.\n */\nexport const hideElements = (elements, root) => {\n if (elements instanceof Array) {\n elements.forEach((elementSelector) => {\n const element = root.querySelector(elementSelector);\n if (element) {\n element.classList.add('d-none');\n }\n });\n } else {\n const element = root.querySelector(elements);\n if (element) {\n element.classList.add('d-none');\n }\n }\n};\n\n/**\n * Checks if the given value is a percentage value.\n *\n * @param {string} value - The value to check.\n * @returns {boolean} True if the value is a percentage value, false otherwise.\n */\nexport const isPercentageValue = (value) => {\n return value.match(/\\d+%/);\n};\n"],"names":["async","templateContext","root","Templates","renderForPromise","then","_ref","html","js","replaceNodeContents","querySelector","catch","error","window","console","log","_ref2","_ref3","_ref4","elements","Array","forEach","elementSelector","element","classList","remove","add","value","match"],"mappings":";;;;;;;4UAgC+BA,MAAMC,gBAAiBC,OAC3CC,mBAAUC,iBAAiB,0CAA2C,IAAIH,kBAChFI,MAAKC,WAACC,KAACA,KAADC,GAAOA,4BACAC,oBAAoBP,KAAKQ,cAAc,gCAAiCH,KAAMC,OAG3FG,OAAMC,QACHC,OAAOC,QAAQC,IAAIH,qCAWMZ,MAAMC,gBAAiBC,OAC7CC,mBAAUC,iBAAiB,iDAAkD,IAAIH,kBACvFI,MAAKW,YAACT,KAACA,KAADC,GAAOA,6BACAC,oBAAoBP,KAAKQ,cAAc,kCAAmCH,KAAMC,OAG7FG,OAAMC,QACHC,OAAOC,QAAQC,IAAIH,oCAWKZ,MAAMC,gBAAiBC,OAC5CC,mBAAUC,iBAAiB,2CAA4C,IAAIH,kBACjFI,MAAKY,YAACV,KAACA,KAADC,GAAOA,6BACAC,oBAAoBP,KAAKQ,cAAc,gCAAiCH,KAAMC,OAG3FG,OAAMC,QACHC,OAAOC,QAAQC,IAAIH,sCAUOZ,MAAMC,gBAAiBC,OAC9CC,mBAAUC,iBAAiB,kDAAmD,IAAIH,kBACxFI,MAAKa,YAACX,KAACA,KAADC,GAAOA,6BACAC,oBAAoBP,KAAKQ,cAAc,kCAAmCH,KAAMC,OAG7FG,OAAMC,QACHC,OAAOC,QAAQC,IAAIH,gCAUC,CAACO,SAAUjB,WAC/BiB,oBAAoBC,MACpBD,SAASE,SAASC,wBACRC,QAAUrB,KAAKQ,cAAcY,iBAC/BC,SACAA,QAAQC,UAAUC,OAAO,iBAG9B,OACGF,QAAUrB,KAAKQ,cAAcS,UAC/BI,SACAA,QAAQC,UAAUC,OAAO,kCAWT,CAACN,SAAUjB,WAC/BiB,oBAAoBC,MACpBD,SAASE,SAASC,wBACRC,QAAUrB,KAAKQ,cAAcY,iBAC/BC,SACAA,QAAQC,UAAUE,IAAI,iBAG3B,OACGH,QAAUrB,KAAKQ,cAAcS,UAC/BI,SACAA,QAAQC,UAAUE,IAAI,uCAWAC,OACvBA,MAAMC,MAAM"} \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imageinsert.min.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imageinsert.min.js new file mode 100755 index 00000000..1b7a7df6 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imageinsert.min.js @@ -0,0 +1,3 @@ +define("tiny_mediacms/imageinsert",["exports","./selectors","core/dropzone","editor_tiny/uploader","core/prefetch","core/str","./common","editor_tiny/options","editor_tiny/utils","tiny_mediacms/imagedetails","tiny_mediacms/imagehelpers"],(function(_exports,_selectors,_dropzone,_uploader,_prefetch,_str,_common,_options,_utils,_imagedetails,_imagehelpers){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.ImageInsert=void 0,_selectors=_interopRequireDefault(_selectors),_dropzone=_interopRequireDefault(_dropzone),_uploader=_interopRequireDefault(_uploader),(0,_prefetch.prefetchStrings)("tiny_mediacms",["insertimage","enterurl","enterurlor","imageurlrequired","uploading","loading","addfilesdrop","sizecustom_help"]);_exports.ImageInsert=class{constructor(_root,editor,currentModal,canShowFilePicker,canShowDropZone){_defineProperty(this,"init",(async function(){const langStringKeys=["insertimage","enterurl","enterurlor","imageurlrequired","uploading","loading","addfilesdrop","sizecustom_help"],langStringvalues=await(0,_str.getStrings)([...langStringKeys].map((key=>({key:key,component:_common.component}))));if(this.langStrings=Object.fromEntries(langStringKeys.map(((key,index)=>[key,langStringvalues[index]]))),this.currentModal.setTitle(this.langStrings.insertimage),this.canShowDropZone){const dropZoneEle=document.querySelector(_selectors.default.IMAGE.elements.dropzoneContainer);let acceptedTypes=(0,_options.getFilePicker)(this.editor,"image").accepted_types;Array.isArray(acceptedTypes)&&(acceptedTypes=acceptedTypes.join(","));const dropZone=new _dropzone.default(dropZoneEle,acceptedTypes,(files=>{this.handleUploadedFile(files)}));dropZone.setLabel(this.langStrings.addfilesdrop),dropZone.init()}await this.registerEventListeners()})),_defineProperty(this,"isValidUrl",(urlString=>!!new RegExp("^(https?:\\/\\/)?((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|((\\d{1,3}\\.){3}\\d{1,3})|localhost)(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*").test(urlString))),_defineProperty(this,"loadPreviewImage",(function(url){this.startImageLoading(),this.currentUrl=url;const image=new Image;image.src=url,image.addEventListener("error",(()=>{this.root.querySelector(_selectors.default.IMAGE.elements.urlWarning).innerHTML=this.langStrings.imageurlrequired,(0,_imagehelpers.showElements)(_selectors.default.IMAGE.elements.urlWarning,this.root),this.currentUrl="",this.stopImageLoading()})),image.addEventListener("load",(()=>{let templateContext={};templateContext.sizecustomhelpicon={text:this.langStrings.sizecustom_help},Promise.all([(0,_imagehelpers.bodyImageDetails)(templateContext,this.root),(0,_imagehelpers.footerImageDetails)(templateContext,this.root)]).then((()=>{new _imagedetails.ImageDetails(this.root,this.editor,this.currentModal,this.canShowFilePicker,this.canShowDropZone,this.currentUrl,image).init()})).then((()=>{this.stopImageLoading()})).catch((error=>{window.console.log(error)}))}))})),_defineProperty(this,"updateLoaderIcon",(function(root,langStrings){let progress=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;const loaderIcon=root.querySelector(_selectors.default.IMAGE.elements.loaderIconContainer+" div");loaderIcon.innerHTML=null!==progress?"".concat(langStrings.uploading," ").concat(Math.round(progress),"%"):langStrings.loading})),_defineProperty(this,"handleUploadedFile",(async files=>{try{this.startImageLoading();const fileURL=await(0,_uploader.default)(this.editor,"image",files[0],files[0].name,(progress=>{this.updateLoaderIcon(this.root,this.langStrings,progress)}));this.updateLoaderIcon(this.root,this.langStrings),this.filePickerCallback({url:fileURL})}catch(error){this.root.querySelector(_selectors.default.IMAGE.elements.urlWarning).innerHTML=void 0!==error.error?error.error:error,(0,_imagehelpers.showElements)(_selectors.default.IMAGE.elements.urlWarning,this.root),this.stopImageLoading()}})),this.root=_root,this.editor=editor,this.currentModal=currentModal,this.canShowFilePicker=canShowFilePicker,this.canShowDropZone=canShowDropZone}toggleUrlButton(){const url=this.root.querySelector(_selectors.default.IMAGE.elements.url).value;this.root.querySelector(_selectors.default.IMAGE.actions.addUrl).disabled=!(""!==url&&this.isValidUrl(url))}urlChanged(){(0,_imagehelpers.hideElements)(_selectors.default.IMAGE.elements.urlWarning,this.root);const input=this.root.querySelector(_selectors.default.IMAGE.elements.url);input.value&&input.value!==this.currentUrl&&this.loadPreviewImage(input.value)}startImageLoading(){(0,_imagehelpers.showElements)(_selectors.default.IMAGE.elements.loaderIcon,this.root);const elementsToHide=[_selectors.default.IMAGE.elements.insertImage,_selectors.default.IMAGE.elements.urlWarning,_selectors.default.IMAGE.elements.modalFooter];(0,_imagehelpers.hideElements)(elementsToHide,this.root)}stopImageLoading(){(0,_imagehelpers.hideElements)(_selectors.default.IMAGE.elements.loaderIcon,this.root);const elementsToShow=[_selectors.default.IMAGE.elements.insertImage,_selectors.default.IMAGE.elements.modalFooter];(0,_imagehelpers.showElements)(elementsToShow,this.root)}filePickerCallback(params){params.url&&this.loadPreviewImage(params.url)}registerEventListeners(){this.root.addEventListener("click",(async e=>{e.target.closest(_selectors.default.IMAGE.actions.addUrl)&&this.urlChanged();if(e.target.closest(_selectors.default.IMAGE.actions.imageBrowser)&&this.canShowFilePicker){e.preventDefault();const params=await(0,_utils.displayFilepicker)(this.editor,"image");this.filePickerCallback(params)}})),this.root.addEventListener("input",(e=>{e.target.closest(_selectors.default.IMAGE.elements.url)&&this.toggleUrlButton()}));const fileInput=this.root.querySelector(_selectors.default.IMAGE.elements.fileInput);fileInput&&fileInput.addEventListener("change",(()=>{this.handleUploadedFile(fileInput.files)}))}}})); + +//# sourceMappingURL=imageinsert.min.js.map \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imageinsert.min.js.map b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imageinsert.min.js.map new file mode 100755 index 00000000..dba4778f --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imageinsert.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"imageinsert.min.js","sources":["../src/imageinsert.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 .\n\n/**\n * Tiny media plugin image insertion class for Moodle.\n *\n * @module tiny_mediacms/imageinsert\n * @copyright 2024 Meirza \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Selectors from './selectors';\nimport Dropzone from 'core/dropzone';\nimport uploadFile from 'editor_tiny/uploader';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getStrings} from 'core/str';\nimport {component} from \"./common\";\nimport {getFilePicker} from 'editor_tiny/options';\nimport {displayFilepicker} from 'editor_tiny/utils';\nimport {ImageDetails} from 'tiny_mediacms/imagedetails';\nimport {\n showElements,\n hideElements,\n bodyImageDetails,\n footerImageDetails,\n} from 'tiny_mediacms/imagehelpers';\n\nprefetchStrings('tiny_mediacms', [\n 'insertimage',\n 'enterurl',\n 'enterurlor',\n 'imageurlrequired',\n 'uploading',\n 'loading',\n 'addfilesdrop',\n 'sizecustom_help',\n]);\n\nexport class ImageInsert {\n\n constructor(\n root,\n editor,\n currentModal,\n canShowFilePicker,\n canShowDropZone,\n ) {\n this.root = root;\n this.editor = editor;\n this.currentModal = currentModal;\n this.canShowFilePicker = canShowFilePicker;\n this.canShowDropZone = canShowDropZone;\n }\n\n init = async function() {\n // Get the localization lang strings and turn them into object.\n const langStringKeys = [\n 'insertimage',\n 'enterurl',\n 'enterurlor',\n 'imageurlrequired',\n 'uploading',\n 'loading',\n 'addfilesdrop',\n 'sizecustom_help',\n ];\n const langStringvalues = await getStrings([...langStringKeys].map((key) => ({key, component})));\n\n // Convert array to object.\n this.langStrings = Object.fromEntries(langStringKeys.map((key, index) => [key, langStringvalues[index]]));\n this.currentModal.setTitle(this.langStrings.insertimage);\n if (this.canShowDropZone) {\n const dropZoneEle = document.querySelector(Selectors.IMAGE.elements.dropzoneContainer);\n\n // Accepted types can be either a string or an array.\n let acceptedTypes = getFilePicker(this.editor, 'image').accepted_types;\n if (Array.isArray(acceptedTypes)) {\n acceptedTypes = acceptedTypes.join(',');\n }\n\n const dropZone = new Dropzone(\n dropZoneEle,\n acceptedTypes,\n files => {\n this.handleUploadedFile(files);\n }\n );\n dropZone.setLabel(this.langStrings.addfilesdrop);\n dropZone.init();\n }\n await this.registerEventListeners();\n };\n\n /**\n * Enables or disables the URL-related buttons in the footer based on the current URL and input value.\n */\n toggleUrlButton() {\n const urlInput = this.root.querySelector(Selectors.IMAGE.elements.url);\n const url = urlInput.value;\n const addUrl = this.root.querySelector(Selectors.IMAGE.actions.addUrl);\n addUrl.disabled = !(url !== \"\" && this.isValidUrl(url));\n }\n\n /**\n * Check if given string is a valid URL.\n *\n * @param {String} urlString URL the link will point to.\n * @returns {boolean} True is valid, otherwise false.\n */\n isValidUrl = urlString => {\n const urlPattern = new RegExp('^(https?:\\\\/\\\\/)?' + // Protocol.\n '((([a-z\\\\d]([a-z\\\\d-]*[a-z\\\\d])*)\\\\.)+[a-z]{2,}|' + // Domain name.\n '((\\\\d{1,3}\\\\.){3}\\\\d{1,3})|localhost)' + // OR ip (v4) address, localhost.\n '(\\\\:\\\\d+)?(\\\\/[-a-z\\\\d%_.~+]*)*'); // Port and path.\n return !!urlPattern.test(urlString);\n };\n\n /**\n * Handles changes in the image URL input field and loads a preview of the image if the URL has changed.\n */\n urlChanged() {\n hideElements(Selectors.IMAGE.elements.urlWarning, this.root);\n const input = this.root.querySelector(Selectors.IMAGE.elements.url);\n if (input.value && input.value !== this.currentUrl) {\n this.loadPreviewImage(input.value);\n }\n }\n\n /**\n * Loads and displays a preview image based on the provided URL, and handles image loading events.\n *\n * @param {string} url - The URL of the image to load and display.\n */\n loadPreviewImage = function(url) {\n this.startImageLoading();\n this.currentUrl = url;\n const image = new Image();\n image.src = url;\n image.addEventListener('error', () => {\n const urlWarningLabelEle = this.root.querySelector(Selectors.IMAGE.elements.urlWarning);\n urlWarningLabelEle.innerHTML = this.langStrings.imageurlrequired;\n showElements(Selectors.IMAGE.elements.urlWarning, this.root);\n this.currentUrl = \"\";\n this.stopImageLoading();\n });\n\n image.addEventListener('load', () => {\n let templateContext = {};\n templateContext.sizecustomhelpicon = {text: this.langStrings.sizecustom_help};\n Promise.all([bodyImageDetails(templateContext, this.root), footerImageDetails(templateContext, this.root)])\n .then(() => {\n const imagedetails = new ImageDetails(\n this.root,\n this.editor,\n this.currentModal,\n this.canShowFilePicker,\n this.canShowDropZone,\n this.currentUrl,\n image,\n );\n imagedetails.init();\n return;\n }).then(() => {\n this.stopImageLoading();\n return;\n })\n .catch(error => {\n window.console.log(error);\n });\n });\n };\n\n /**\n * Displays the upload loader and disables UI elements while loading a file.\n */\n startImageLoading() {\n showElements(Selectors.IMAGE.elements.loaderIcon, this.root);\n const elementsToHide = [\n Selectors.IMAGE.elements.insertImage,\n Selectors.IMAGE.elements.urlWarning,\n Selectors.IMAGE.elements.modalFooter,\n ];\n hideElements(elementsToHide, this.root);\n }\n\n /**\n * Displays the upload loader and disables UI elements while loading a file.\n */\n stopImageLoading() {\n hideElements(Selectors.IMAGE.elements.loaderIcon, this.root);\n const elementsToShow = [\n Selectors.IMAGE.elements.insertImage,\n Selectors.IMAGE.elements.modalFooter,\n ];\n showElements(elementsToShow, this.root);\n }\n\n filePickerCallback(params) {\n if (params.url) {\n this.loadPreviewImage(params.url);\n }\n }\n\n /**\n * Updates the content of the loader icon.\n *\n * @param {HTMLElement} root - The root element containing the loader icon.\n * @param {object} langStrings - An object containing language strings.\n * @param {number|null} progress - The progress percentage (optional).\n * @returns {void}\n */\n updateLoaderIcon = (root, langStrings, progress = null) => {\n const loaderIcon = root.querySelector(Selectors.IMAGE.elements.loaderIconContainer + ' div');\n loaderIcon.innerHTML = progress !== null ? `${langStrings.uploading} ${Math.round(progress)}%` : langStrings.loading;\n };\n\n /**\n * Handles the uploaded file, initiates the upload process, and updates the UI during the upload.\n *\n * @param {FileList} files - The list of files to upload (usually from a file input field).\n * @returns {Promise} A promise that resolves when the file is uploaded and processed.\n */\n handleUploadedFile = async(files) => {\n try {\n this.startImageLoading();\n const fileURL = await uploadFile(this.editor, 'image', files[0], files[0].name, (progress) => {\n this.updateLoaderIcon(this.root, this.langStrings, progress);\n });\n // Set the loader icon content to \"loading\" after the file upload completes.\n this.updateLoaderIcon(this.root, this.langStrings);\n this.filePickerCallback({url: fileURL});\n } catch (error) {\n // Handle the error.\n const urlWarningLabelEle = this.root.querySelector(Selectors.IMAGE.elements.urlWarning);\n urlWarningLabelEle.innerHTML = error.error !== undefined ? error.error : error;\n showElements(Selectors.IMAGE.elements.urlWarning, this.root);\n this.stopImageLoading();\n }\n };\n\n registerEventListeners() {\n this.root.addEventListener('click', async(e) => {\n const addUrlEle = e.target.closest(Selectors.IMAGE.actions.addUrl);\n if (addUrlEle) {\n this.urlChanged();\n }\n\n const imageBrowserAction = e.target.closest(Selectors.IMAGE.actions.imageBrowser);\n if (imageBrowserAction && this.canShowFilePicker) {\n e.preventDefault();\n const params = await displayFilepicker(this.editor, 'image');\n this.filePickerCallback(params);\n }\n });\n\n this.root.addEventListener('input', (e) => {\n const urlEle = e.target.closest(Selectors.IMAGE.elements.url);\n if (urlEle) {\n this.toggleUrlButton();\n }\n });\n\n const fileInput = this.root.querySelector(Selectors.IMAGE.elements.fileInput);\n if (fileInput) {\n fileInput.addEventListener('change', () => {\n this.handleUploadedFile(fileInput.files);\n });\n }\n }\n}"],"names":["constructor","root","editor","currentModal","canShowFilePicker","canShowDropZone","async","langStringKeys","langStringvalues","map","key","component","langStrings","Object","fromEntries","index","setTitle","this","insertimage","dropZoneEle","document","querySelector","Selectors","IMAGE","elements","dropzoneContainer","acceptedTypes","accepted_types","Array","isArray","join","dropZone","Dropzone","files","handleUploadedFile","setLabel","addfilesdrop","init","registerEventListeners","urlString","RegExp","test","url","startImageLoading","currentUrl","image","Image","src","addEventListener","urlWarning","innerHTML","imageurlrequired","stopImageLoading","templateContext","sizecustomhelpicon","text","sizecustom_help","Promise","all","then","ImageDetails","catch","error","window","console","log","progress","loaderIcon","loaderIconContainer","uploading","Math","round","loading","fileURL","name","updateLoaderIcon","filePickerCallback","undefined","toggleUrlButton","value","actions","addUrl","disabled","isValidUrl","urlChanged","input","loadPreviewImage","elementsToHide","insertImage","modalFooter","elementsToShow","params","e","target","closest","imageBrowser","preventDefault","fileInput"],"mappings":"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"} \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imagemodal.min.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imagemodal.min.js new file mode 100755 index 00000000..fbd7b061 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imagemodal.min.js @@ -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 \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imagemodal.min.js.map b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/imagemodal.min.js.map new file mode 100755 index 00000000..47669ad8 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/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 .\n\n/**\n * Image Modal for Tiny.\n *\n * @module tiny_mediacms/imagemodal\n * @copyright 2022 Huong Nguyen \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"} \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/manager.min.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/manager.min.js new file mode 100755 index 00000000..dd736626 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/manager.min.js @@ -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 \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/manager.min.js.map b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/manager.min.js.map new file mode 100755 index 00000000..2fc47acf --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/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 .\n\n/**\n * Tiny Media Manager plugin class for Moodle.\n *\n * @module tiny_mediacms/manager\n * @copyright 2022, Stevani Andolo \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"} \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/options.min.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/options.min.js new file mode 100755 index 00000000..ed1f7e68 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/options.min.js @@ -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 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +const dataName=(0,_options.getPluginOptionName)(_common.pluginName,"data"),permissionsName=(0,_options.getPluginOptionName)(_common.pluginName,"permissions"),ltiName=(0,_options.getPluginOptionName)(_common.pluginName,"lti");_exports.register=editor=>{const registerOption=editor.options.register;registerOption(permissionsName,{processor:"object",default:{image:{filepicker:!1}}}),registerOption(dataName,{processor:"object",default:{mediacmsApiUrl:"",mediacmsBaseUrl:"",mediacmsPageSize:12,autoConvertEnabled:!0,autoConvertBaseUrl:"",autoConvertOptions:{showTitle:!0,linkTitle:!0,showRelated:!0,showUserAvatar:!0}}}),registerOption(ltiName,{processor:"object",default:{toolId:0,courseId:0,contentItemUrl:""}})};const getPermissions=editor=>editor.options.get(permissionsName);_exports.getPermissions=getPermissions;_exports.getImagePermissions=editor=>getPermissions(editor).image;_exports.getEmbedPermissions=editor=>getPermissions(editor).embed;_exports.getData=editor=>editor.options.get(dataName);_exports.getLti=editor=>editor.options.get(ltiName)})); + +//# sourceMappingURL=options.min.js.map \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/options.min.js.map b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/options.min.js.map new file mode 100755 index 00000000..545b140d --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/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 .\n\n/**\n * Options helper for Tiny Media plugin.\n *\n * @module tiny_mediacms/options\n * @copyright 2022 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getPluginOptionName} from 'editor_tiny/options';\nimport {pluginName} from './common';\n\nconst dataName = getPluginOptionName(pluginName, 'data');\nconst permissionsName = getPluginOptionName(pluginName, 'permissions');\nconst ltiName = getPluginOptionName(pluginName, 'lti');\n\n/**\n * Register the options for the Tiny Media plugin.\n *\n * @param {TinyMCE} editor\n */\nexport const register = (editor) => {\n const registerOption = editor.options.register;\n\n registerOption(permissionsName, {\n processor: 'object',\n \"default\": {\n image: {\n filepicker: false,\n }\n },\n });\n\n registerOption(dataName, {\n processor: 'object',\n \"default\": {\n // MediaCMS video library configuration\n mediacmsApiUrl: '', // e.g., 'https://deic.mediacms.io/api/v1/media'\n mediacmsBaseUrl: '', // e.g., 'https://deic.mediacms.io'\n mediacmsPageSize: 12,\n // Auto-conversion settings\n autoConvertEnabled: true, // Enable/disable auto-conversion of pasted MediaCMS URLs\n autoConvertBaseUrl: '', // Base URL to restrict auto-conversion (empty = allow all MediaCMS domains)\n autoConvertOptions: {\n // Default embed options for auto-converted videos\n showTitle: true,\n linkTitle: true,\n showRelated: true,\n showUserAvatar: true,\n },\n },\n });\n\n registerOption(ltiName, {\n processor: 'object',\n \"default\": {\n // LTI configuration for MediaCMS iframe library\n toolId: 0, // LTI external tool ID\n courseId: 0, // Current course ID\n contentItemUrl: '', // URL to /mod/lti/contentitem.php for Deep Linking\n },\n });\n};\n\n/**\n * Get the permissions configuration for the Tiny Media plugin.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getPermissions = (editor) => editor.options.get(permissionsName);\n\n/**\n * Get the permissions configuration for the Tiny Media plugin.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getImagePermissions = (editor) => getPermissions(editor).image;\n\n/**\n * Get the permissions configuration for the Tiny Media plugin.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getEmbedPermissions = (editor) => getPermissions(editor).embed;\n\n/**\n * Get the data configuration for the Media Manager.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getData = (editor) => editor.options.get(dataName);\n\n/**\n * Get the LTI configuration for the MediaCMS iframe library.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getLti = (editor) => editor.options.get(ltiName);\n"],"names":["dataName","pluginName","permissionsName","ltiName","editor","registerOption","options","register","processor","image","filepicker","mediacmsApiUrl","mediacmsBaseUrl","mediacmsPageSize","autoConvertEnabled","autoConvertBaseUrl","autoConvertOptions","showTitle","linkTitle","showRelated","showUserAvatar","toolId","courseId","contentItemUrl","getPermissions","get","embed"],"mappings":";;;;;;;;MA0BMA,UAAW,gCAAoBC,mBAAY,QAC3CC,iBAAkB,gCAAoBD,mBAAY,eAClDE,SAAU,gCAAoBF,mBAAY,yBAOvBG,eACfC,eAAiBD,OAAOE,QAAQC,SAEtCF,eAAeH,gBAAiB,CAC5BM,UAAW,iBACA,CACPC,MAAO,CACHC,YAAY,MAKxBL,eAAeL,SAAU,CACrBQ,UAAW,iBACA,CAEPG,eAAgB,GAChBC,gBAAiB,GACjBC,iBAAkB,GAElBC,oBAAoB,EACpBC,mBAAoB,GACpBC,mBAAoB,CAEhBC,WAAW,EACXC,WAAW,EACXC,aAAa,EACbC,gBAAgB,MAK5Bf,eAAeF,QAAS,CACpBK,UAAW,iBACA,CAEPa,OAAQ,EACRC,SAAU,EACVC,eAAgB,aAWfC,eAAkBpB,QAAWA,OAAOE,QAAQmB,IAAIvB,qFAQzBE,QAAWoB,eAAepB,QAAQK,mCAQlCL,QAAWoB,eAAepB,QAAQsB,uBAQ9CtB,QAAWA,OAAOE,QAAQmB,IAAIzB,0BAQ/BI,QAAWA,OAAOE,QAAQmB,IAAItB"} \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/plugin.min.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/plugin.min.js new file mode 100755 index 00000000..bb3c9e1d --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/plugin.min.js @@ -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 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,Commands=_interopRequireWildcard(Commands),Configuration=_interopRequireWildcard(Configuration),Options=_interopRequireWildcard(Options);var _default=new Promise((async resolve=>{const[tinyMCE,setupCommands,pluginMetadata]=await Promise.all([(0,_loader.getTinyMCE)(),Commands.getSetup(),(0,_utils.getPluginMetadata)(_common.component,_common.pluginName)]);tinyMCE.PluginManager.add("".concat(_common.component,"/plugin"),(editor=>(Options.register(editor),setupCommands(editor),(0,_autoconvert.setupAutoConvert)(editor),editor.on("GetContent",(e=>{if("html"===e.format){const tempDiv=document.createElement("div");tempDiv.innerHTML=e.content,tempDiv.querySelectorAll(".tiny-mediacms-edit-btn").forEach((btn=>btn.remove())),tempDiv.querySelectorAll(".tiny-mediacms-iframe-wrapper").forEach((wrapper=>{const iframe=wrapper.querySelector("iframe");iframe&&wrapper.parentNode.insertBefore(iframe,wrapper),wrapper.remove()})),tempDiv.querySelectorAll(".tiny-iframe-responsive").forEach((wrapper=>{const iframe=wrapper.querySelector("iframe");iframe&&wrapper.parentNode.insertBefore(iframe,wrapper),wrapper.remove()})),e.content=tempDiv.innerHTML}})),pluginMetadata))),resolve(["".concat(_common.component,"/plugin"),Configuration])}));return _exports.default=_default,_exports.default})); + +//# sourceMappingURL=plugin.min.js.map \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/plugin.min.js.map b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/plugin.min.js.map new file mode 100755 index 00000000..ad58087b --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/plugin.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"plugin.min.js","sources":["../src/plugin.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Tiny Media plugin for Moodle.\n *\n * @module tiny_mediacms/plugin\n * @copyright 2022 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {getTinyMCE} from 'editor_tiny/loader';\nimport {getPluginMetadata} from 'editor_tiny/utils';\n\nimport {component, pluginName} from './common';\nimport * as Commands from './commands';\nimport * as Configuration from './configuration';\nimport * as Options from './options';\nimport {setupAutoConvert} from './autoconvert';\n\n// eslint-disable-next-line no-async-promise-executor\nexport default new Promise(async(resolve) => {\n const [\n tinyMCE,\n setupCommands,\n pluginMetadata,\n ] = await Promise.all([\n getTinyMCE(),\n Commands.getSetup(),\n getPluginMetadata(component, pluginName),\n ]);\n\n tinyMCE.PluginManager.add(`${component}/plugin`, (editor) => {\n // Register options.\n Options.register(editor);\n\n // Setup the Commands (buttons, menu items, and so on).\n setupCommands(editor);\n\n // Setup auto-conversion of pasted MediaCMS URLs.\n setupAutoConvert(editor);\n\n // Clean up editor-only elements before content is saved.\n // Remove wrapper divs and edit buttons that are only for the editor UI.\n editor.on('GetContent', (e) => {\n if (e.format === 'html') {\n // Create a temporary container to manipulate the HTML\n const tempDiv = document.createElement('div');\n tempDiv.innerHTML = e.content;\n\n // Remove edit buttons\n tempDiv.querySelectorAll('.tiny-mediacms-edit-btn').forEach(btn => btn.remove());\n\n // Unwrap iframes from tiny-mediacms-iframe-wrapper\n tempDiv.querySelectorAll('.tiny-mediacms-iframe-wrapper').forEach(wrapper => {\n const iframe = wrapper.querySelector('iframe');\n if (iframe) {\n wrapper.parentNode.insertBefore(iframe, wrapper);\n }\n wrapper.remove();\n });\n\n // Unwrap iframes from tiny-iframe-responsive\n tempDiv.querySelectorAll('.tiny-iframe-responsive').forEach(wrapper => {\n const iframe = wrapper.querySelector('iframe');\n if (iframe) {\n wrapper.parentNode.insertBefore(iframe, wrapper);\n }\n wrapper.remove();\n });\n\n e.content = tempDiv.innerHTML;\n }\n });\n\n return pluginMetadata;\n });\n\n // Resolve the Media Plugin and include configuration.\n resolve([`${component}/plugin`, Configuration]);\n});\n"],"names":["Promise","async","tinyMCE","setupCommands","pluginMetadata","all","Commands","getSetup","component","pluginName","PluginManager","add","editor","Options","register","on","e","format","tempDiv","document","createElement","innerHTML","content","querySelectorAll","forEach","btn","remove","wrapper","iframe","querySelector","parentNode","insertBefore","resolve","Configuration"],"mappings":";;;;;;;2OAgCe,IAAIA,SAAQC,MAAAA,gBAEnBC,QACAC,cACAC,sBACMJ,QAAQK,IAAI,EAClB,wBACAC,SAASC,YACT,4BAAkBC,kBAAWC,sBAGjCP,QAAQQ,cAAcC,cAAOH,8BAAqBI,SAE9CC,QAAQC,SAASF,QAGjBT,cAAcS,0CAGGA,QAIjBA,OAAOG,GAAG,cAAeC,OACJ,SAAbA,EAAEC,OAAmB,OAEfC,QAAUC,SAASC,cAAc,OACvCF,QAAQG,UAAYL,EAAEM,QAGtBJ,QAAQK,iBAAiB,2BAA2BC,SAAQC,KAAOA,IAAIC,WAGvER,QAAQK,iBAAiB,iCAAiCC,SAAQG,gBACxDC,OAASD,QAAQE,cAAc,UACjCD,QACAD,QAAQG,WAAWC,aAAaH,OAAQD,SAE5CA,QAAQD,YAIZR,QAAQK,iBAAiB,2BAA2BC,SAAQG,gBAClDC,OAASD,QAAQE,cAAc,UACjCD,QACAD,QAAQG,WAAWC,aAAaH,OAAQD,SAE5CA,QAAQD,YAGZV,EAAEM,QAAUJ,QAAQG,cAIrBjB,kBAIX4B,QAAQ,WAAIxB,6BAAoByB"} \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/selectors.min.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/selectors.min.js new file mode 100755 index 00000000..06c5ee30 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/selectors.min.js @@ -0,0 +1,3 @@ +define("tiny_mediacms/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={IMAGE:{actions:{submit:".tiny_imagecms_urlentrysubmit",imageBrowser:".openimagecmsbrowser",addUrl:".tiny_imagecms_addurl",deleteImage:".tiny_imagecms_deleteicon"},elements:{form:"form.tiny_imagecms_form",alignSettings:".tiny_imagecms_button",alt:".tiny_imagecms_altentry",altWarning:".tiny_imagecms_altwarning",height:".tiny_imagecms_heightentry",width:".tiny_imagecms_widthentry",url:".tiny_imagecms_urlentry",urlWarning:".tiny_imagecms_urlwarning",size:".tiny_imagecms_size",presentation:".tiny_imagecms_presentation",constrain:".tiny_imagecms_constrain",customStyle:".tiny_imagecms_customstyle",preview:".tiny_imagecms_preview",previewBox:".tiny_imagecms_preview_box",loaderIcon:".tiny_imagecms_loader",loaderIconContainer:".tiny_imagecms_loader_container",insertImage:".tiny_imagecms_insert_image",modalFooter:".modal-footer",dropzoneContainer:".tiny_imagecms_dropzone_container",fileInput:"#tiny_imagecms_fileinput",fileNameLabel:".tiny_imagecms_filename",sizeOriginal:".tiny_imagecms_sizeoriginal",sizeCustom:".tiny_imagecms_sizecustom",properties:".tiny_imagecms_properties"},styles:{responsive:"img-fluid"}},EMBED:{actions:{submit:".tiny_mediacms_submit",mediaBrowser:".openmediacmsbrowser"},elements:{form:"form.tiny_mediacms_form",source:".tiny_mediacms_source",track:".tiny_mediacms_track",mediaSource:".tiny_mediacms_media_source",linkSource:".tiny_mediacms_link_source",linkSize:".tiny_mediacms_link_size",posterSource:".tiny_mediacms_poster_source",posterSize:".tiny_mediacms_poster_size",displayOptions:".tiny_mediacms_display_options",name:".tiny_mediacms_name_entry",title:".tiny_mediacms_title_entry",url:".tiny_mediacms_url_entry",width:".tiny_mediacms_width_entry",height:".tiny_mediacms_height_entry",trackSource:".tiny_mediacms_track_source",trackKind:".tiny_mediacms_track_kind_entry",trackLabel:".tiny_mediacms_track_label_entry",trackLang:".tiny_mediacms_track_lang_entry",trackDefault:".tiny_mediacms_track_default",mediaControl:".tiny_mediacms_controls",mediaAutoplay:".tiny_mediacms_autoplay",mediaMute:".tiny_mediacms_mute",mediaLoop:".tiny_mediacms_loop",advancedSettings:".tiny_mediacms_advancedsettings",linkTab:'li[data-medium-type="link"]',videoTab:'li[data-medium-type="video"]',audioTab:'li[data-medium-type="audio"]',linkPane:'.tab-pane[data-medium-type="link"]',videoPane:'.tab-pane[data-medium-type="video"]',audioPane:'.tab-pane[data-medium-type="audio"]',trackSubtitlesTab:'li[data-track-kind="subtitles"]',trackCaptionsTab:'li[data-track-kind="captions"]',trackDescriptionsTab:'li[data-track-kind="descriptions"]',trackChaptersTab:'li[data-track-kind="chapters"]',trackMetadataTab:'li[data-track-kind="metadata"]',trackSubtitlesPane:'.tab-pane[data-track-kind="subtitles"]',trackCaptionsPane:'.tab-pane[data-track-kind="captions"]',trackDescriptionsPane:'.tab-pane[data-track-kind="descriptions"]',trackChaptersPane:'.tab-pane[data-track-kind="chapters"]',trackMetadataPane:'.tab-pane[data-track-kind="metadata"]'},mediaTypes:{link:"LINK",video:"VIDEO",audio:"AUDIO"},trackKinds:{subtitles:"SUBTITLES",captions:"CAPTIONS",descriptions:"DESCRIPTIONS",chapters:"CHAPTERS",metadata:"METADATA"}},IFRAME:{actions:{remove:'[data-action="remove"]'},elements:{form:"form.tiny_iframecms_form",url:".tiny_iframecms_url",urlWarning:".tiny_iframecms_url_warning",showTitle:".tiny_iframecms_showtitle",linkTitle:".tiny_iframecms_linktitle",showRelated:".tiny_iframecms_showrelated",showUserAvatar:".tiny_iframecms_showuseravatar",responsive:".tiny_iframecms_responsive",startAt:".tiny_iframecms_startat",startAtEnabled:".tiny_iframecms_startat_enabled",aspectRatio:".tiny_iframecms_aspectratio",width:".tiny_iframecms_width",height:".tiny_iframecms_height",preview:".tiny_iframecms_preview",previewContainer:".tiny_iframecms_preview_container",tabs:".tiny_iframecms_tabs",tabUrlBtn:".tiny_iframecms_tab_url_btn",tabIframeLibraryBtn:".tiny_iframecms_tab_iframe_library_btn",paneUrl:".tiny_iframecms_pane_url",paneIframeLibrary:".tiny_iframecms_pane_iframe_library",iframeLibraryContainer:".tiny_iframecms_iframe_library_container",iframeLibraryPlaceholder:".tiny_iframecms_iframe_library_placeholder",iframeLibraryLoading:".tiny_iframecms_iframe_library_loading",iframeLibraryFrame:".tiny_iframecms_iframe_library_frame"},aspectRatios:{"16:9":{width:560,height:315},"4:3":{width:560,height:420},"1:1":{width:400,height:400},custom:null}}},_exports.default})); + +//# sourceMappingURL=selectors.min.js.map \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/selectors.min.js.map b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/selectors.min.js.map new file mode 100755 index 00000000..e90972a5 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/selectors.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"selectors.min.js","sources":["../src/selectors.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 .\n\n/**\n * Tiny Media plugin helper function to build queryable data selectors.\n *\n * @module tiny_mediacms/selectors\n * @copyright 2022 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n IMAGE: {\n actions: {\n submit: '.tiny_imagecms_urlentrysubmit',\n imageBrowser: '.openimagecmsbrowser',\n addUrl: '.tiny_imagecms_addurl',\n deleteImage: '.tiny_imagecms_deleteicon',\n },\n elements: {\n form: 'form.tiny_imagecms_form',\n alignSettings: '.tiny_imagecms_button',\n alt: '.tiny_imagecms_altentry',\n altWarning: '.tiny_imagecms_altwarning',\n height: '.tiny_imagecms_heightentry',\n width: '.tiny_imagecms_widthentry',\n url: '.tiny_imagecms_urlentry',\n urlWarning: '.tiny_imagecms_urlwarning',\n size: '.tiny_imagecms_size',\n presentation: '.tiny_imagecms_presentation',\n constrain: '.tiny_imagecms_constrain',\n customStyle: '.tiny_imagecms_customstyle',\n preview: '.tiny_imagecms_preview',\n previewBox: '.tiny_imagecms_preview_box',\n loaderIcon: '.tiny_imagecms_loader',\n loaderIconContainer: '.tiny_imagecms_loader_container',\n insertImage: '.tiny_imagecms_insert_image',\n modalFooter: '.modal-footer',\n dropzoneContainer: '.tiny_imagecms_dropzone_container',\n fileInput: '#tiny_imagecms_fileinput',\n fileNameLabel: '.tiny_imagecms_filename',\n sizeOriginal: '.tiny_imagecms_sizeoriginal',\n sizeCustom: '.tiny_imagecms_sizecustom',\n properties: '.tiny_imagecms_properties',\n },\n styles: {\n responsive: 'img-fluid',\n },\n },\n EMBED: {\n actions: {\n submit: '.tiny_mediacms_submit',\n mediaBrowser: '.openmediacmsbrowser',\n },\n elements: {\n form: 'form.tiny_mediacms_form',\n source: '.tiny_mediacms_source',\n track: '.tiny_mediacms_track',\n mediaSource: '.tiny_mediacms_media_source',\n linkSource: '.tiny_mediacms_link_source',\n linkSize: '.tiny_mediacms_link_size',\n posterSource: '.tiny_mediacms_poster_source',\n posterSize: '.tiny_mediacms_poster_size',\n displayOptions: '.tiny_mediacms_display_options',\n name: '.tiny_mediacms_name_entry',\n title: '.tiny_mediacms_title_entry',\n url: '.tiny_mediacms_url_entry',\n width: '.tiny_mediacms_width_entry',\n height: '.tiny_mediacms_height_entry',\n trackSource: '.tiny_mediacms_track_source',\n trackKind: '.tiny_mediacms_track_kind_entry',\n trackLabel: '.tiny_mediacms_track_label_entry',\n trackLang: '.tiny_mediacms_track_lang_entry',\n trackDefault: '.tiny_mediacms_track_default',\n mediaControl: '.tiny_mediacms_controls',\n mediaAutoplay: '.tiny_mediacms_autoplay',\n mediaMute: '.tiny_mediacms_mute',\n mediaLoop: '.tiny_mediacms_loop',\n advancedSettings: '.tiny_mediacms_advancedsettings',\n linkTab: 'li[data-medium-type=\"link\"]',\n videoTab: 'li[data-medium-type=\"video\"]',\n audioTab: 'li[data-medium-type=\"audio\"]',\n linkPane: '.tab-pane[data-medium-type=\"link\"]',\n videoPane: '.tab-pane[data-medium-type=\"video\"]',\n audioPane: '.tab-pane[data-medium-type=\"audio\"]',\n trackSubtitlesTab: 'li[data-track-kind=\"subtitles\"]',\n trackCaptionsTab: 'li[data-track-kind=\"captions\"]',\n trackDescriptionsTab: 'li[data-track-kind=\"descriptions\"]',\n trackChaptersTab: 'li[data-track-kind=\"chapters\"]',\n trackMetadataTab: 'li[data-track-kind=\"metadata\"]',\n trackSubtitlesPane: '.tab-pane[data-track-kind=\"subtitles\"]',\n trackCaptionsPane: '.tab-pane[data-track-kind=\"captions\"]',\n trackDescriptionsPane: '.tab-pane[data-track-kind=\"descriptions\"]',\n trackChaptersPane: '.tab-pane[data-track-kind=\"chapters\"]',\n trackMetadataPane: '.tab-pane[data-track-kind=\"metadata\"]',\n },\n mediaTypes: {\n link: 'LINK',\n video: 'VIDEO',\n audio: 'AUDIO',\n },\n trackKinds: {\n subtitles: 'SUBTITLES',\n captions: 'CAPTIONS',\n descriptions: 'DESCRIPTIONS',\n chapters: 'CHAPTERS',\n metadata: 'METADATA',\n },\n },\n IFRAME: {\n actions: {\n remove: '[data-action=\"remove\"]',\n },\n elements: {\n form: 'form.tiny_iframecms_form',\n url: '.tiny_iframecms_url',\n urlWarning: '.tiny_iframecms_url_warning',\n showTitle: '.tiny_iframecms_showtitle',\n linkTitle: '.tiny_iframecms_linktitle',\n showRelated: '.tiny_iframecms_showrelated',\n showUserAvatar: '.tiny_iframecms_showuseravatar',\n responsive: '.tiny_iframecms_responsive',\n startAt: '.tiny_iframecms_startat',\n startAtEnabled: '.tiny_iframecms_startat_enabled',\n aspectRatio: '.tiny_iframecms_aspectratio',\n width: '.tiny_iframecms_width',\n height: '.tiny_iframecms_height',\n preview: '.tiny_iframecms_preview',\n previewContainer: '.tiny_iframecms_preview_container',\n // Tab elements\n tabs: '.tiny_iframecms_tabs',\n tabUrlBtn: '.tiny_iframecms_tab_url_btn',\n tabIframeLibraryBtn: '.tiny_iframecms_tab_iframe_library_btn',\n paneUrl: '.tiny_iframecms_pane_url',\n paneIframeLibrary: '.tiny_iframecms_pane_iframe_library',\n // Iframe library elements\n iframeLibraryContainer: '.tiny_iframecms_iframe_library_container',\n iframeLibraryPlaceholder:\n '.tiny_iframecms_iframe_library_placeholder',\n iframeLibraryLoading: '.tiny_iframecms_iframe_library_loading',\n iframeLibraryFrame: '.tiny_iframecms_iframe_library_frame',\n },\n aspectRatios: {\n '16:9': { width: 560, height: 315 },\n '4:3': { width: 560, height: 420 },\n '1:1': { width: 400, height: 400 },\n custom: null,\n },\n },\n};\n"],"names":["IMAGE","actions","submit","imageBrowser","addUrl","deleteImage","elements","form","alignSettings","alt","altWarning","height","width","url","urlWarning","size","presentation","constrain","customStyle","preview","previewBox","loaderIcon","loaderIconContainer","insertImage","modalFooter","dropzoneContainer","fileInput","fileNameLabel","sizeOriginal","sizeCustom","properties","styles","responsive","EMBED","mediaBrowser","source","track","mediaSource","linkSource","linkSize","posterSource","posterSize","displayOptions","name","title","trackSource","trackKind","trackLabel","trackLang","trackDefault","mediaControl","mediaAutoplay","mediaMute","mediaLoop","advancedSettings","linkTab","videoTab","audioTab","linkPane","videoPane","audioPane","trackSubtitlesTab","trackCaptionsTab","trackDescriptionsTab","trackChaptersTab","trackMetadataTab","trackSubtitlesPane","trackCaptionsPane","trackDescriptionsPane","trackChaptersPane","trackMetadataPane","mediaTypes","link","video","audio","trackKinds","subtitles","captions","descriptions","chapters","metadata","IFRAME","remove","showTitle","linkTitle","showRelated","showUserAvatar","startAt","startAtEnabled","aspectRatio","previewContainer","tabs","tabUrlBtn","tabIframeLibraryBtn","paneUrl","paneIframeLibrary","iframeLibraryContainer","iframeLibraryPlaceholder","iframeLibraryLoading","iframeLibraryFrame","aspectRatios","custom"],"mappings":"yKAuBe,CACXA,MAAO,CACHC,QAAS,CACLC,OAAQ,gCACRC,aAAc,uBACdC,OAAQ,wBACRC,YAAa,6BAEjBC,SAAU,CACNC,KAAM,0BACNC,cAAe,wBACfC,IAAK,0BACLC,WAAY,4BACZC,OAAQ,6BACRC,MAAO,4BACPC,IAAK,0BACLC,WAAY,4BACZC,KAAM,sBACNC,aAAc,8BACdC,UAAW,2BACXC,YAAa,6BACbC,QAAS,yBACTC,WAAY,6BACZC,WAAY,wBACZC,oBAAqB,kCACrBC,YAAa,8BACbC,YAAa,gBACbC,kBAAmB,oCACnBC,UAAW,2BACXC,cAAe,0BACfC,aAAc,8BACdC,WAAY,4BACZC,WAAY,6BAEhBC,OAAQ,CACJC,WAAY,cAGpBC,MAAO,CACHhC,QAAS,CACLC,OAAQ,wBACRgC,aAAc,wBAElB5B,SAAU,CACNC,KAAM,0BACN4B,OAAQ,wBACRC,MAAO,uBACPC,YAAa,8BACbC,WAAY,6BACZC,SAAU,2BACVC,aAAc,+BACdC,WAAY,6BACZC,eAAgB,iCAChBC,KAAM,4BACNC,MAAO,6BACP/B,IAAK,2BACLD,MAAO,6BACPD,OAAQ,8BACRkC,YAAa,8BACbC,UAAW,kCACXC,WAAY,mCACZC,UAAW,kCACXC,aAAc,+BACdC,aAAc,0BACdC,cAAe,0BACfC,UAAW,sBACXC,UAAW,sBACXC,iBAAkB,kCAClBC,QAAS,8BACTC,SAAU,+BACVC,SAAU,+BACVC,SAAU,qCACVC,UAAW,sCACXC,UAAW,sCACXC,kBAAmB,kCACnBC,iBAAkB,iCAClBC,qBAAsB,qCACtBC,iBAAkB,iCAClBC,iBAAkB,iCAClBC,mBAAoB,yCACpBC,kBAAmB,wCACnBC,sBAAuB,4CACvBC,kBAAmB,wCACnBC,kBAAmB,yCAEvBC,WAAY,CACRC,KAAM,OACNC,MAAO,QACPC,MAAO,SAEXC,WAAY,CACRC,UAAW,YACXC,SAAU,WACVC,aAAc,eACdC,SAAU,WACVC,SAAU,aAGlBC,OAAQ,CACJhF,QAAS,CACLiF,OAAQ,0BAEZ5E,SAAU,CACNC,KAAM,2BACNM,IAAK,sBACLC,WAAY,8BACZqE,UAAW,4BACXC,UAAW,4BACXC,YAAa,8BACbC,eAAgB,iCAChBtD,WAAY,6BACZuD,QAAS,0BACTC,eAAgB,kCAChBC,YAAa,8BACb7E,MAAO,wBACPD,OAAQ,yBACRQ,QAAS,0BACTuE,iBAAkB,oCAElBC,KAAM,uBACNC,UAAW,8BACXC,oBAAqB,yCACrBC,QAAS,2BACTC,kBAAmB,sCAEnBC,uBAAwB,2CACxBC,yBACI,6CACJC,qBAAsB,yCACtBC,mBAAoB,wCAExBC,aAAc,QACF,CAAExF,MAAO,IAAKD,OAAQ,WACvB,CAAEC,MAAO,IAAKD,OAAQ,WACtB,CAAEC,MAAO,IAAKD,OAAQ,KAC7B0F,OAAQ"} \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/usedfiles.min.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/usedfiles.min.js new file mode 100755 index 00000000..2e03a1ff --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/usedfiles.min.js @@ -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 + * @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,"\\$&")+"(?.+?)[\\?\"']","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 \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/usedfiles.min.js.map b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/usedfiles.min.js.map new file mode 100755 index 00000000..1af09ca8 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/build/usedfiles.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"usedfiles.min.js","sources":["../src/usedfiles.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 .\n\n/**\n * Tiny Media Manager usedfiles.\n *\n * @module tiny_mediacms/usedfiles\n * @copyright 2022, Stevani Andolo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as Templates from 'core/templates';\nimport Config from 'core/config';\n\nclass UsedFileManager {\n constructor(files, userContext, itemId, elementId) {\n this.files = files;\n this.userContext = userContext;\n this.itemId = itemId;\n this.elementId = elementId;\n }\n\n getElementId() {\n return this.elementId;\n }\n\n getUsedFiles() {\n const editor = window.parent.tinymce.EditorManager.get(this.getElementId());\n if (!editor) {\n window.console.error(`Editor not found for ${this.getElementId()}`);\n return [];\n }\n const content = editor.getContent();\n const baseUrl = `${Config.wwwroot}/draftfile.php/${this.userContext}/user/draft/${this.itemId}/`;\n const pattern = new RegExp(\"[\\\"']\" + baseUrl.replace(/[-/\\\\^$*+?.()|[\\]{}]/g, '\\\\$&') + \"(?.+?)[\\\\?\\\"']\", 'gm');\n\n const usedFiles = [...content.matchAll(pattern)].map((match) => decodeURIComponent(match.groups.filename));\n\n return usedFiles;\n }\n\n // Return an array of unused files.\n findUnusedFiles(usedFiles) {\n return Object.entries(this.files)\n .filter(([filename]) => !usedFiles.includes(filename))\n .map(([filename]) => filename);\n }\n\n // Return an array of missing files.\n findMissingFiles(usedFiles) {\n return usedFiles.filter((filename) => !this.files.hasOwnProperty(filename));\n }\n\n updateFiles() {\n const form = document.querySelector('form');\n const usedFiles = this.getUsedFiles();\n const unusedFiles = this.findUnusedFiles(usedFiles);\n const missingFiles = this.findMissingFiles(usedFiles);\n\n form.querySelectorAll('input[type=checkbox][name^=\"deletefile\"]').forEach((checkbox) => {\n if (!unusedFiles.includes(checkbox.dataset.filename)) {\n checkbox.closest('.fitem').remove();\n }\n });\n\n form.classList.toggle('has-missing-files', !!missingFiles.length);\n form.classList.toggle('has-unused-files', !!unusedFiles.length);\n\n return Templates.renderForPromise('tiny_mediacms/missingfiles', {\n missingFiles,\n }).then(({html, js}) => {\n Templates.replaceNodeContents(form.querySelector('.missing-files'), html, js);\n return;\n });\n }\n}\n\nexport const init = (files, usercontext, itemid, elementid) => {\n const manager = new UsedFileManager(files, usercontext, itemid, elementid);\n manager.updateFiles();\n\n return manager;\n};\n"],"names":["UsedFileManager","constructor","files","userContext","itemId","elementId","getElementId","this","getUsedFiles","editor","window","parent","tinymce","EditorManager","get","console","error","content","getContent","baseUrl","Config","wwwroot","pattern","RegExp","replace","matchAll","map","match","decodeURIComponent","groups","filename","findUnusedFiles","usedFiles","Object","entries","filter","_ref","includes","_ref2","findMissingFiles","hasOwnProperty","updateFiles","form","document","querySelector","unusedFiles","missingFiles","querySelectorAll","forEach","checkbox","dataset","closest","remove","classList","toggle","length","Templates","renderForPromise","then","_ref3","html","js","replaceNodeContents","usercontext","itemid","elementid","manager"],"mappings":";;;;;;;+EA0BMA,gBACFC,YAAYC,MAAOC,YAAaC,OAAQC,gBAC/BH,MAAQA,WACRC,YAAcA,iBACdC,OAASA,YACTC,UAAYA,UAGrBC,sBACWC,KAAKF,UAGhBG,qBACUC,OAASC,OAAOC,OAAOC,QAAQC,cAAcC,IAAIP,KAAKD,oBACvDG,cACDC,OAAOK,QAAQC,qCAA8BT,KAAKD,iBAC3C,SAELW,QAAUR,OAAOS,aACjBC,kBAAaC,gBAAOC,kCAAyBd,KAAKJ,mCAA0BI,KAAKH,YACjFkB,QAAU,IAAIC,OAAO,QAAUJ,QAAQK,QAAQ,wBAAyB,QAAU,2BAA4B,YAElG,IAAIP,QAAQQ,SAASH,UAAUI,KAAKC,OAAUC,mBAAmBD,MAAME,OAAOC,YAMpGC,gBAAgBC,kBACLC,OAAOC,QAAQ3B,KAAKL,OACtBiC,QAAOC,WAAEN,sBAAeE,UAAUK,SAASP,aAC3CJ,KAAIY,YAAER,uBAAcA,YAI7BS,iBAAiBP,kBACNA,UAAUG,QAAQL,WAAcvB,KAAKL,MAAMsC,eAAeV,YAGrEW,oBACUC,KAAOC,SAASC,cAAc,QAC9BZ,UAAYzB,KAAKC,eACjBqC,YAActC,KAAKwB,gBAAgBC,WACnCc,aAAevC,KAAKgC,iBAAiBP,kBAE3CU,KAAKK,iBAAiB,4CAA4CC,SAASC,WAClEJ,YAAYR,SAASY,SAASC,QAAQpB,WACvCmB,SAASE,QAAQ,UAAUC,YAInCV,KAAKW,UAAUC,OAAO,sBAAuBR,aAAaS,QAC1Db,KAAKW,UAAUC,OAAO,qBAAsBT,YAAYU,QAEjDC,UAAUC,iBAAiB,6BAA8B,CAC5DX,aAAAA,eACDY,MAAKC,YAACC,KAACA,KAADC,GAAOA,UACZL,UAAUM,oBAAoBpB,KAAKE,cAAc,kBAAmBgB,KAAMC,sBAMlE,CAAC3D,MAAO6D,YAAaC,OAAQC,mBACvCC,QAAU,IAAIlE,gBAAgBE,MAAO6D,YAAaC,OAAQC,kBAChEC,QAAQzB,cAEDyB"} \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/autoconvert.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/autoconvert.js new file mode 100755 index 00000000..2285daee --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/autoconvert.js @@ -0,0 +1,265 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Tiny MediaCMS Auto-convert module. + * + * This module automatically converts pasted MediaCMS URLs into embedded videos. + * When a user pastes a MediaCMS video URL (e.g., https://deic.mediacms.io/view?m=JpBd1Zvdl), + * it will be automatically converted to an iframe embed. + * + * @module tiny_mediacms/autoconvert + * @copyright 2024 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {getData} from './options'; + +/** + * Regular expression patterns for MediaCMS URLs. + * Matches URLs like: + * - https://deic.mediacms.io/view?m=JpBd1Zvdl + * - https://example.mediacms.io/view?m=VIDEO_ID + * - Custom domains configured in the plugin + */ +const MEDIACMS_VIEW_URL_PATTERN = /^(https?:\/\/[^\/]+)\/view\?m=([a-zA-Z0-9_-]+)$/; + +/** + * Check if a string is a valid MediaCMS view URL. + * + * @param {string} text - The text to check + * @returns {Object|null} - Parsed URL info or null if not a valid MediaCMS URL + */ +const parseMediaCMSUrl = (text) => { + if (!text || typeof text !== 'string') { + return null; + } + + const trimmed = text.trim(); + + // Check for MediaCMS view URL pattern + const match = trimmed.match(MEDIACMS_VIEW_URL_PATTERN); + if (match) { + return { + baseUrl: match[1], + videoId: match[2], + originalUrl: trimmed, + }; + } + + return null; +}; + +/** + * Check if the pasted URL's domain is allowed based on configuration. + * + * @param {Object} parsed - Parsed URL info + * @param {Object} config - Plugin configuration + * @returns {boolean} - True if the domain is allowed + */ +const isDomainAllowed = (parsed, config) => { + // If no specific base URL is configured, allow all MediaCMS domains + const configuredBaseUrl = config.autoConvertBaseUrl || config.mediacmsBaseUrl; + if (!configuredBaseUrl) { + return true; + } + + // Check if the URL's base matches the configured base URL + try { + const configuredUrl = new URL(configuredBaseUrl); + const pastedUrl = new URL(parsed.baseUrl); + return configuredUrl.host === pastedUrl.host; + } catch (e) { + // If URL parsing fails, allow the conversion + return true; + } +}; + +/** + * Generate the iframe embed HTML for a MediaCMS video. + * + * @param {Object} parsed - Parsed URL info + * @param {Object} options - Embed options + * @returns {string} - The iframe HTML + */ +const generateEmbedHtml = (parsed, options = {}) => { + // Build the embed URL with default options + const embedUrl = new URL(`${parsed.baseUrl}/embed`); + embedUrl.searchParams.set('m', parsed.videoId); + + // Apply default options (all enabled by default for best user experience) + embedUrl.searchParams.set('showTitle', options.showTitle !== false ? '1' : '0'); + embedUrl.searchParams.set('showRelated', options.showRelated !== false ? '1' : '0'); + embedUrl.searchParams.set('showUserAvatar', options.showUserAvatar !== false ? '1' : '0'); + embedUrl.searchParams.set('linkTitle', options.linkTitle !== false ? '1' : '0'); + + // Generate clean iframe HTML (wrapper will be added by editor for UI, then stripped on save) + const html = ``; + + 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); +}; diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/commands.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/commands.js new file mode 100755 index 00000000..27ad9b76 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/commands.js @@ -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 . + +/** + * Tiny Media commands. + * + * @module tiny_mediacms/commands + * @copyright 2022 Huong Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {getStrings} from 'core/str'; +import { + component, + iframeButtonName, + iframeMenuItemName, + iframeIcon, +} from './common'; +import IframeEmbed from './iframeembed'; +import {getButtonImage} from 'editor_tiny/utils'; + +const isIframe = (node) => node.nodeName.toLowerCase() === 'iframe' || + (node.classList && node.classList.contains('tiny-iframe-responsive')) || + (node.classList && node.classList.contains('tiny-mediacms-iframe-wrapper')); + +/** + * Wrap iframes with overlay containers that allow hover detection. + * Since iframes capture mouse events, we add an invisible overlay on top + * that shows the edit button on hover. + * + * @param {TinyMCE} editor - The editor instance + * @param {Function} handleIframeAction - The action to perform when clicking the button + */ +const setupIframeOverlays = (editor, handleIframeAction) => { + /** + * Process all iframes in the editor and add overlay wrappers. + */ + const processIframes = () => { + const editorBody = editor.getBody(); + if (!editorBody) { + return; + } + + const iframes = editorBody.querySelectorAll('iframe'); + iframes.forEach((iframe) => { + // Skip if already wrapped + if (iframe.parentElement?.classList.contains('tiny-mediacms-iframe-wrapper')) { + return; + } + + // Skip TinyMCE internal iframes + if (iframe.hasAttribute('data-mce-object') || iframe.hasAttribute('data-mce-placeholder')) { + return; + } + + // Create wrapper div + const wrapper = editor.getDoc().createElement('div'); + wrapper.className = 'tiny-mediacms-iframe-wrapper'; + wrapper.setAttribute('contenteditable', 'false'); + + // Create edit button (positioned inside wrapper, over the iframe) + const editBtn = editor.getDoc().createElement('button'); + editBtn.className = 'tiny-mediacms-edit-btn'; + editBtn.setAttribute('type', 'button'); + editBtn.setAttribute('title', 'Edit video embed options'); + // Use clean inline SVG to avoid TinyMCE wrapper issues + editBtn.innerHTML = '' + + '' + + '' + + ''; + + // Wrap the iframe: insert wrapper, move iframe into it, add button + iframe.parentNode.insertBefore(wrapper, iframe); + wrapper.appendChild(iframe); + wrapper.appendChild(editBtn); + }); + }; + + /** + * Add CSS styles for hover effects to the editor's document. + */ + const addStyles = () => { + const editorDoc = editor.getDoc(); + if (!editorDoc) { + return; + } + + // Check if styles already added + if (editorDoc.getElementById('tiny-mediacms-overlay-styles')) { + return; + } + + const style = editorDoc.createElement('style'); + style.id = 'tiny-mediacms-overlay-styles'; + style.textContent = ` + .tiny-mediacms-iframe-wrapper { + display: inline-block; + position: relative; + line-height: 0; + vertical-align: top; + } + .tiny-mediacms-iframe-wrapper iframe { + display: block; + } + .tiny-mediacms-edit-btn { + position: absolute; + top: 48px; + left: 6px; + width: 28px; + height: 28px; + background: #ffffff; + border: none; + border-radius: 50%; + cursor: pointer; + z-index: 10; + padding: 0; + margin: 0; + box-shadow: 0 2px 6px rgba(0,0,0,0.35); + transition: transform 0.15s, box-shadow 0.15s; + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + } + .tiny-mediacms-edit-btn:hover { + transform: scale(1.15); + box-shadow: 0 3px 10px rgba(0,0,0,0.45); + } + .tiny-mediacms-edit-btn svg { + width: 18px !important; + height: 18px !important; + display: block !important; + } + `; + editorDoc.head.appendChild(style); + }; + + /** + * Handle click on the edit button. + * + * @param {Event} e - The click event + */ + const handleOverlayClick = (e) => { + const target = e.target; + + // Check if clicked on edit button or its child (svg/path) + const editBtn = target.closest('.tiny-mediacms-edit-btn'); + if (!editBtn) { + return; + } + + e.preventDefault(); + e.stopPropagation(); + + // Find the associated wrapper and iframe + const wrapper = editBtn.closest('.tiny-mediacms-iframe-wrapper'); + if (!wrapper) { + return; + } + + const iframe = wrapper.querySelector('iframe'); + if (!iframe) { + return; + } + + // Select the wrapper so TinyMCE knows which element is selected + editor.selection.select(wrapper); + + // Open the edit dialog + handleIframeAction(); + }; + + // Setup on editor init + editor.on('init', () => { + addStyles(); + processIframes(); + + // Handle clicks on the overlay + editor.getBody().addEventListener('click', handleOverlayClick); + }); + + // Re-process when content changes + editor.on('SetContent', () => { + processIframes(); + }); + + // Re-process when content is pasted + editor.on('PastePostProcess', () => { + setTimeout(processIframes, 100); + }); + + // Re-process after undo/redo + editor.on('Undo Redo', () => { + processIframes(); + }); + + // Re-process on any content change (covers modal updates) + editor.on('Change', () => { + setTimeout(processIframes, 50); + }); + + // Re-process when node changes (selection changes) + editor.on('NodeChange', () => { + processIframes(); + }); +}; + +const registerIframeCommand = (editor, iframeButtonText, iframeButtonImage) => { + const handleIframeAction = () => { + const iframeEmbed = new IframeEmbed(editor); + iframeEmbed.displayDialogue(); + }; + + // Register the iframe icon + editor.ui.registry.addIcon(iframeIcon, iframeButtonImage.html); + + // Register the Menu Button as a toggle. + // This means that when highlighted over an existing iframe element it will show as toggled on. + editor.ui.registry.addToggleButton(iframeButtonName, { + icon: iframeIcon, + tooltip: iframeButtonText, + onAction: handleIframeAction, + onSetup: api => { + return editor.selection.selectorChangedWithUnbind( + 'iframe:not([data-mce-object]):not([data-mce-placeholder]),.tiny-iframe-responsive,.tiny-mediacms-iframe-wrapper', + api.setActive + ).unbind; + } + }); + + editor.ui.registry.addMenuItem(iframeMenuItemName, { + icon: iframeIcon, + text: iframeButtonText, + onAction: handleIframeAction, + }); + + editor.ui.registry.addContextToolbar(iframeButtonName, { + predicate: isIframe, + items: iframeButtonName, + position: 'node', + scope: 'node' + }); + + editor.ui.registry.addContextMenu(iframeButtonName, { + update: isIframe, + }); + + // Setup iframe overlays with edit button on hover + setupIframeOverlays(editor, handleIframeAction); +}; + +export const getSetup = async() => { + const [ + iframeButtonText, + ] = await getStrings([ + 'iframebuttontitle', + ].map((key) => ({key, component}))); + + const [ + iframeButtonImage, + ] = await Promise.all([ + getButtonImage('icon', component), + ]); + + // Note: The function returned here must be synchronous and cannot use promises. + // All promises must be resolved prior to returning the function. + return (editor) => { + registerIframeCommand(editor, iframeButtonText, iframeButtonImage); + }; +}; diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/common.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/common.js new file mode 100755 index 00000000..cf20c3c0 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/common.js @@ -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 . + +/** + * Tiny Media common values. + * + * @module tiny_mediacms/common + * @copyright 2022 Huong Nguyen + * @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', +}; diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/configuration.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/configuration.js new file mode 100755 index 00000000..3eb2569a --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/configuration.js @@ -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 . + +/** + * Tiny Media configuration. + * + * @module tiny_mediacms/configuration + * @copyright 2022 Huong Nguyen + * @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), + }; +}; diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/embed.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/embed.js new file mode 100755 index 00000000..c31563b3 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/embed.js @@ -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 . + +/** + * Tiny Media plugin Embed class for Moodle. + * + * @module tiny_mediacms/embed + * @copyright 2022 Huong Nguyen + * @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; + } + }); + }); + } +} diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/embedmodal.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/embedmodal.js new file mode 100755 index 00000000..96d86853 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/embedmodal.js @@ -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 . + +/** + * Embedded Media Management Modal for Tiny. + * + * @module tiny_mediacms/embedmodal + * @copyright 2022 Andrew Lyons + * @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); + } +} diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/iframeembed.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/iframeembed.js new file mode 100755 index 00000000..4a3b135a --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/iframeembed.js @@ -0,0 +1,1090 @@ +// 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 . + +/** + * Tiny Media2 Iframe Embed class. + * + * @module tiny_mediacms/iframeembed + * @copyright 2024 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Templates from 'core/templates'; +import { getString } from 'core/str'; +import * as ModalEvents from 'core/modal_events'; +import { component } from './common'; +import IframeModal from './iframemodal'; +import Selectors from './selectors'; +import { getLti } from './options'; + +export default class IframeEmbed { + editor = null; + currentModal = null; + isUpdating = false; + selectedIframe = null; + debounceTimer = null; + // Iframe library state + iframeLibraryLoaded = false; + selectedLibraryVideo = null; + iframeLibraryUrl = + 'https://temp.web357.com/mediacms/deic-mediacms-embed-videos.html'; + + constructor(editor) { + this.editor = editor; + } + + /** + * Parse input to extract video URL or iframe src. + * Handles: iframe embed code, view URL, or embed URL. + * + * @param {string} input - The user input (URL or embed code) + * @returns {Object|null} Parsed URL info or null if invalid + */ + parseInput(input) { + if (!input || !input.trim()) { + return null; + } + + input = input.trim(); + + // Check if it's an iframe embed code + const iframeMatch = input.match( + /]*src=["']([^"']+)["'][^>]*>/i, + ); + if (iframeMatch) { + return this.parseEmbedUrl(iframeMatch[1]); + } + + // Check if it's a URL + if (input.startsWith('http://') || input.startsWith('https://')) { + return this.parseVideoUrl(input); + } + + return null; + } + + /** + * Parse a video view URL and convert to embed format. + * + * @param {string} url - The video URL + * @returns {Object|null} Parsed info + */ + parseVideoUrl(url) { + try { + const urlObj = new URL(url); + const baseUrl = `${urlObj.protocol}//${urlObj.host}`; + + // MediaCMS view URL: /view?m=VIDEO_ID + if (urlObj.pathname === '/view' && urlObj.searchParams.has('m')) { + return { + baseUrl: baseUrl, + videoId: urlObj.searchParams.get('m'), + isEmbed: false, + }; + } + + // MediaCMS embed URL: /embed?m=VIDEO_ID&options + if (urlObj.pathname === '/embed' && urlObj.searchParams.has('m')) { + const tParam = urlObj.searchParams.get('t'); + return { + baseUrl: baseUrl, + videoId: urlObj.searchParams.get('m'), + isEmbed: true, + showTitle: urlObj.searchParams.get('showTitle') === '1', + linkTitle: urlObj.searchParams.get('linkTitle') === '1', + showRelated: urlObj.searchParams.get('showRelated') === '1', + showUserAvatar: + urlObj.searchParams.get('showUserAvatar') === '1', + startAt: tParam + ? this.secondsToTimeString(parseInt(tParam)) + : null, + }; + } + + // Generic URL - just use as-is + return { + baseUrl: baseUrl, + rawUrl: url, + isGeneric: true, + }; + } catch (e) { + return null; + } + } + + /** + * Parse an embed URL. + * + * @param {string} url - The embed URL + * @returns {Object|null} Parsed info + */ + parseEmbedUrl(url) { + return this.parseVideoUrl(url); + } + + /** + * Convert seconds to time string (M:SS format). + * + * @param {number} seconds - Time in seconds + * @returns {string} Time string + */ + secondsToTimeString(seconds) { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}:${secs.toString().padStart(2, '0')}`; + } + + /** + * Convert time string to seconds. + * + * @param {string} timeStr - Time string (M:SS or just seconds) + * @returns {number|null} Seconds or null if invalid + */ + timeStringToSeconds(timeStr) { + if (!timeStr || !timeStr.trim()) { + return null; + } + timeStr = timeStr.trim(); + + // Handle M:SS format + if (timeStr.includes(':')) { + const parts = timeStr.split(':'); + const mins = parseInt(parts[0]) || 0; + const secs = parseInt(parts[1]) || 0; + return mins * 60 + secs; + } + + // Handle plain seconds + const secs = parseInt(timeStr); + return isNaN(secs) ? null : secs; + } + + /** + * Build the embed URL with options. + * + * @param {Object} parsed - Parsed URL info + * @param {Object} options - Embed options + * @returns {string} The complete embed URL + */ + buildEmbedUrl(parsed, options) { + if (parsed.isGeneric) { + return parsed.rawUrl; + } + + const url = new URL(`${parsed.baseUrl}/embed`); + url.searchParams.set('m', parsed.videoId); + + // Always include all options with 1 or 0 + url.searchParams.set('showTitle', options.showTitle ? '1' : '0'); + url.searchParams.set('showRelated', options.showRelated ? '1' : '0'); + url.searchParams.set( + 'showUserAvatar', + options.showUserAvatar ? '1' : '0', + ); + url.searchParams.set('linkTitle', options.linkTitle ? '1' : '0'); + + // Add start time if enabled + if (options.startAtEnabled && options.startAt) { + const seconds = this.timeStringToSeconds(options.startAt); + if (seconds !== null && seconds > 0) { + url.searchParams.set('t', seconds.toString()); + } + } + + return url.toString(); + } + + /** + * Get the template context for the modal. + * + * @param {Object} data - Existing data for updating + * @returns {Object} Template context + */ + async getTemplateContext(data = {}) { + return { + elementid: this.editor.getElement().id, + isupdating: this.isUpdating, + url: data.url || '', + showTitle: data.showTitle !== false, + linkTitle: data.linkTitle !== false, + showRelated: data.showRelated !== false, + showUserAvatar: data.showUserAvatar !== false, + responsive: data.responsive !== false, + startAtEnabled: data.startAtEnabled || false, + startAt: data.startAt || '0:00', + width: data.width || 560, + height: data.height || 315, + is16_9: !data.aspectRatio || data.aspectRatio === '16:9', + is4_3: data.aspectRatio === '4:3', + is1_1: data.aspectRatio === '1:1', + isCustom: data.aspectRatio === 'custom', + }; + } + + /** + * Display the iframe embed dialog. + */ + async displayDialogue() { + this.selectedIframe = this.getSelectedIframe(); + const data = this.getCurrentIframeData(); + this.isUpdating = data !== null; + + // Reset iframe library state for new modal + this.iframeLibraryLoaded = false; + + this.currentModal = await IframeModal.create({ + title: getString('iframemodaltitle', component), + templateContext: await this.getTemplateContext(data || {}), + }); + + await this.registerEventListeners(this.currentModal); + } + + /** + * Get the currently selected iframe in the editor. + * + * @returns {HTMLElement|null} The iframe element or null + */ + getSelectedIframe() { + const node = this.editor.selection.getNode(); + + if (node.nodeName.toLowerCase() === 'iframe') { + return node; + } + + // Check if selection contains an iframe wrapper (including overlay wrapper) + const iframe = node.querySelector('iframe'); + if (iframe) { + return iframe; + } + + // Check if we're on the overlay or wrapper and need to find the iframe + const wrapper = + node.closest('.tiny-mediacms-iframe-wrapper') || + node.closest('.tiny-iframe-responsive'); + if (wrapper) { + return wrapper.querySelector('iframe'); + } + + return null; + } + + /** + * Get current iframe data for editing. + * + * @returns {Object|null} Current iframe data or null + */ + getCurrentIframeData() { + if (!this.selectedIframe) { + return null; + } + + const src = this.selectedIframe.getAttribute('src'); + const parsed = this.parseInput(src); + + // Check if responsive by looking at style + const style = this.selectedIframe.getAttribute('style') || ''; + const isResponsive = style.includes('aspect-ratio'); + + return { + url: src, + width: this.selectedIframe.getAttribute('width') || 560, + height: this.selectedIframe.getAttribute('height') || 315, + showTitle: parsed?.showTitle ?? true, + linkTitle: parsed?.linkTitle ?? true, + showRelated: parsed?.showRelated ?? true, + showUserAvatar: parsed?.showUserAvatar ?? true, + responsive: isResponsive, + startAtEnabled: parsed?.startAt !== null, + startAt: parsed?.startAt || '0:00', + }; + } + + /** + * Get form values from the modal. + * + * @param {HTMLElement} root - Modal root element + * @returns {Object} Form values + */ + getFormValues(root) { + const form = root.querySelector(Selectors.IFRAME.elements.form); + + return { + url: form.querySelector(Selectors.IFRAME.elements.url).value.trim(), + showTitle: form.querySelector(Selectors.IFRAME.elements.showTitle) + .checked, + linkTitle: form.querySelector(Selectors.IFRAME.elements.linkTitle) + .checked, + showRelated: form.querySelector( + Selectors.IFRAME.elements.showRelated, + ).checked, + showUserAvatar: form.querySelector( + Selectors.IFRAME.elements.showUserAvatar, + ).checked, + responsive: form.querySelector(Selectors.IFRAME.elements.responsive) + .checked, + startAtEnabled: form.querySelector( + Selectors.IFRAME.elements.startAtEnabled, + ).checked, + startAt: form + .querySelector(Selectors.IFRAME.elements.startAt) + .value.trim(), + aspectRatio: form.querySelector( + Selectors.IFRAME.elements.aspectRatio, + ).value, + width: + parseInt( + form.querySelector(Selectors.IFRAME.elements.width).value, + ) || 560, + height: + parseInt( + form.querySelector(Selectors.IFRAME.elements.height).value, + ) || 315, + }; + } + + /** + * Generate the iframe HTML. + * + * @param {Object} values - Form values + * @returns {Promise} Generated HTML + */ + async generateIframeHtml(values) { + const parsed = this.parseInput(values.url); + if (!parsed) { + return ''; + } + + const embedUrl = this.buildEmbedUrl(parsed, values); + + // Calculate aspect ratio values for CSS + const aspectRatioCalcs = { + '16:9': '16 / 9', + '4:3': '4 / 3', + '1:1': '1 / 1', + custom: `${values.width} / ${values.height}`, + }; + + const context = { + src: embedUrl, + width: values.width, + height: values.height, + responsive: values.responsive, + aspectRatioCalc: aspectRatioCalcs[values.aspectRatio] || '16 / 9', + aspectRatioValue: aspectRatioCalcs[values.aspectRatio] || '16 / 9', + }; + + const { html } = await Templates.renderForPromise( + 'tiny_mediacms/iframe_embed_output', + context, + ); + return html; + } + + /** + * Update the preview in the modal. + * + * @param {HTMLElement} root - Modal root element + */ + async updatePreview(root) { + const values = this.getFormValues(root); + const previewContainer = root.querySelector( + Selectors.IFRAME.elements.preview, + ); + const urlWarning = root.querySelector( + Selectors.IFRAME.elements.urlWarning, + ); + + if (!values.url) { + previewContainer.innerHTML = + 'Enter a video URL to see preview'; + urlWarning.classList.add('d-none'); + return; + } + + const parsed = this.parseInput(values.url); + if (!parsed) { + previewContainer.innerHTML = + 'Invalid URL format'; + urlWarning.classList.remove('d-none'); + return; + } + + urlWarning.classList.add('d-none'); + const embedUrl = this.buildEmbedUrl(parsed, values); + + // Show a scaled preview + const previewWidth = Math.min(values.width, 400); + const scale = previewWidth / values.width; + const previewHeight = Math.round(values.height * scale); + + previewContainer.innerHTML = ` + + `; + } + + /** + * Handle form input changes with debounce. + * + * @param {HTMLElement} root - Modal root element + */ + handleInputChange(root) { + clearTimeout(this.debounceTimer); + this.debounceTimer = setTimeout(() => { + this.updatePreview(root); + }, 500); + } + + /** + * Handle aspect ratio change - update dimensions. + * + * @param {HTMLElement} root - Modal root element + */ + handleAspectRatioChange(root) { + const form = root.querySelector(Selectors.IFRAME.elements.form); + const aspectRatio = form.querySelector( + Selectors.IFRAME.elements.aspectRatio, + ).value; + const dimensions = Selectors.IFRAME.aspectRatios[aspectRatio]; + + // Only update dimensions for predefined ratios, not 'custom' + if (dimensions && aspectRatio !== 'custom') { + form.querySelector(Selectors.IFRAME.elements.width).value = + dimensions.width; + form.querySelector(Selectors.IFRAME.elements.height).value = + dimensions.height; + } + + this.updatePreview(root); + } + + /** + * Handle dialog submission. + * + * @param {Object} modal - The modal instance + */ + async handleDialogueSubmission(modal) { + const root = modal.getRoot()[0]; + const values = this.getFormValues(root); + + if (!values.url) { + return; + } + + const html = await this.generateIframeHtml(values); + if (html) { + if (this.isUpdating && this.selectedIframe) { + // Replace existing iframe (including wrapper if present) + // Check for both old wrapper and new overlay wrapper + const wrapper = + this.selectedIframe.closest( + '.tiny-mediacms-iframe-wrapper', + ) || this.selectedIframe.closest('.tiny-iframe-responsive'); + if (wrapper) { + wrapper.outerHTML = html; + } else { + this.selectedIframe.outerHTML = html; + } + this.isUpdating = false; + // Fire change event to trigger overlay reprocessing + this.editor.fire('Change'); + } else { + this.editor.insertContent(html); + } + } + } + + /** + * Handle video removal from the editor. + * + * @param {Object} modal - The modal instance + */ + async handleRemove(modal) { + // Get confirmation string + const confirmMessage = await getString( + 'removeiframeconfirm', + component, + ); + + // Show confirmation dialog + // eslint-disable-next-line no-alert + if (!window.confirm(confirmMessage)) { + return; + } + + if (this.selectedIframe) { + // Remove the iframe (including wrapper if present) + const wrapper = + this.selectedIframe.closest('.tiny-mediacms-iframe-wrapper') || + this.selectedIframe.closest('.tiny-iframe-responsive'); + if (wrapper) { + wrapper.remove(); + } else { + this.selectedIframe.remove(); + } + } + + // Close the modal + this.isUpdating = false; + modal.hide(); + } + + /** + * Register event listeners for the modal. + * + * @param {Object} modal - The modal instance + */ + async registerEventListeners(modal) { + await modal.getBody(); + const $root = modal.getRoot(); + const root = $root[0]; + + // Input change handlers + const form = root.querySelector(Selectors.IFRAME.elements.form); + + // URL input + form.querySelector(Selectors.IFRAME.elements.url).addEventListener( + 'input', + () => this.handleInputChange(root), + ); + + // Checkbox options + [ + Selectors.IFRAME.elements.showTitle, + Selectors.IFRAME.elements.linkTitle, + Selectors.IFRAME.elements.showRelated, + Selectors.IFRAME.elements.showUserAvatar, + Selectors.IFRAME.elements.responsive, + Selectors.IFRAME.elements.startAtEnabled, + ].forEach((selector) => { + form.querySelector(selector).addEventListener('change', () => + this.handleInputChange(root), + ); + }); + + // Start at time input + form.querySelector(Selectors.IFRAME.elements.startAt).addEventListener( + 'input', + () => this.handleInputChange(root), + ); + + // Aspect ratio change + form.querySelector( + Selectors.IFRAME.elements.aspectRatio, + ).addEventListener('change', () => this.handleAspectRatioChange(root)); + + // Dimension inputs + form.querySelector(Selectors.IFRAME.elements.width).addEventListener( + 'input', + () => this.handleInputChange(root), + ); + form.querySelector(Selectors.IFRAME.elements.height).addEventListener( + 'input', + () => this.handleInputChange(root), + ); + + // Modal events + $root.on(ModalEvents.save, () => this.handleDialogueSubmission(modal)); + $root.on(ModalEvents.hidden, () => { + this.currentModal.destroy(); + }); + + // Remove button handler (only present when updating) + const removeBtn = root.querySelector(Selectors.IFRAME.actions.remove); + if (removeBtn) { + removeBtn.addEventListener('click', () => this.handleRemove(modal)); + } + + // Initial preview if we have a URL + const urlInput = form.querySelector(Selectors.IFRAME.elements.url); + if (urlInput.value) { + this.updatePreview(root); + } + + // Tab change handler - load iframe library when switching to iframe library tab + const iframeLibraryTabBtn = form.querySelector( + Selectors.IFRAME.elements.tabIframeLibraryBtn, + ); + if (iframeLibraryTabBtn) { + iframeLibraryTabBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + + // Manually handle tab switching for Bootstrap 5 + this.switchToIframeLibraryTab(root); + + // Small delay to ensure tab pane is visible before loading iframe + setTimeout(() => this.handleIframeLibraryTabClick(root), 100); + }); + // Also handle Bootstrap tab events (if Bootstrap handles it) + iframeLibraryTabBtn.addEventListener('shown.bs.tab', () => + this.handleIframeLibraryTabClick(root), + ); + // jQuery Bootstrap 4 event + const $iframeLibraryTabBtn = window.jQuery + ? window.jQuery(iframeLibraryTabBtn) + : null; + if ($iframeLibraryTabBtn) { + $iframeLibraryTabBtn.on('shown.bs.tab', () => + this.handleIframeLibraryTabClick(root), + ); + } + } + + // Tab change handler for URL tab + const urlTabBtn = form.querySelector( + Selectors.IFRAME.elements.tabUrlBtn, + ); + if (urlTabBtn) { + urlTabBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + + // Manually handle tab switching + this.switchToUrlTab(root); + }); + } + + // Iframe library event listeners + this.registerIframeLibraryEventListeners(root); + } + + /** + * Switch to the URL tab. + * + * @param {HTMLElement} root - Modal root element + */ + switchToUrlTab(root) { + const form = root.querySelector(Selectors.IFRAME.elements.form); + + // Get tab buttons + const urlTabBtn = form.querySelector(Selectors.IFRAME.elements.tabUrlBtn); + const iframeLibraryTabBtn = form.querySelector(Selectors.IFRAME.elements.tabIframeLibraryBtn); + + // Get tab panes + const urlPane = form.querySelector(Selectors.IFRAME.elements.paneUrl); + const iframeLibraryPane = form.querySelector(Selectors.IFRAME.elements.paneIframeLibrary); + + // Update tab button states + if (urlTabBtn) { + urlTabBtn.classList.add('active'); + urlTabBtn.setAttribute('aria-selected', 'true'); + } + if (iframeLibraryTabBtn) { + iframeLibraryTabBtn.classList.remove('active'); + iframeLibraryTabBtn.setAttribute('aria-selected', 'false'); + } + + // Update tab pane visibility + if (urlPane) { + urlPane.classList.add('show', 'active'); + } + if (iframeLibraryPane) { + iframeLibraryPane.classList.remove('show', 'active'); + } + } + + /** + * Switch to the iframe library tab. + * + * @param {HTMLElement} root - Modal root element + */ + switchToIframeLibraryTab(root) { + const form = root.querySelector(Selectors.IFRAME.elements.form); + + // Get tab buttons + const urlTabBtn = form.querySelector(Selectors.IFRAME.elements.tabUrlBtn); + const iframeLibraryTabBtn = form.querySelector(Selectors.IFRAME.elements.tabIframeLibraryBtn); + + // Get tab panes + const urlPane = form.querySelector(Selectors.IFRAME.elements.paneUrl); + const iframeLibraryPane = form.querySelector(Selectors.IFRAME.elements.paneIframeLibrary); + + // Update tab button states + if (urlTabBtn) { + urlTabBtn.classList.remove('active'); + urlTabBtn.setAttribute('aria-selected', 'false'); + } + if (iframeLibraryTabBtn) { + iframeLibraryTabBtn.classList.add('active'); + iframeLibraryTabBtn.setAttribute('aria-selected', 'true'); + } + + // Update tab pane visibility + if (urlPane) { + urlPane.classList.remove('show', 'active'); + } + if (iframeLibraryPane) { + iframeLibraryPane.classList.add('show', 'active'); + } + } + + /** + * Register event listeners for the iframe library. + * + * @param {HTMLElement} root - Modal root element + */ + registerIframeLibraryEventListeners(root) { + // Listen for messages from the iframe (for video selection) + window.addEventListener('message', (event) => { + this.handleIframeLibraryMessage(root, event); + }); + } + + /** + * Handle iframe library tab click - load iframe if not already loaded. + * + * @param {HTMLElement} root - Modal root element + */ + handleIframeLibraryTabClick(root) { + const form = root.querySelector(Selectors.IFRAME.elements.form); + const pane = form.querySelector( + Selectors.IFRAME.elements.paneIframeLibrary, + ); + const iframeEl = pane + ? pane.querySelector(Selectors.IFRAME.elements.iframeLibraryFrame) + : null; + + // eslint-disable-next-line no-console + console.log( + 'handleIframeLibraryTabClick called, iframeLibraryLoaded:', + this.iframeLibraryLoaded, + 'iframe src:', + iframeEl ? iframeEl.src : 'no iframe', + 'pane:', + pane, + ); + + // Load if not loaded or iframe has no source or empty source + if ( + !this.iframeLibraryLoaded || + (iframeEl && (!iframeEl.src || iframeEl.src === '')) + ) { + this.loadIframeLibrary(root); + } + } + + /** + * Load the iframe library using LTI flow or fallback to static URL. + * + * @param {HTMLElement} root - Modal root element + */ + loadIframeLibrary(root) { + const ltiConfig = getLti(this.editor); + + // eslint-disable-next-line no-console + console.log('loadIframeLibrary called, LTI config:', ltiConfig); + + // Check if LTI is configured with a content item URL + if (ltiConfig?.contentItemUrl) { + this.loadIframeLibraryViaLti(root, ltiConfig); + } else { + // Fallback to static URL if LTI not configured + this.loadIframeLibraryStatic(root); + } + } + + /** + * Load the iframe library via LTI Deep Linking (Content Item Selection). + * Sets the iframe src to contentitem.php which initiates the LTI Deep Linking flow. + * This sends an LtiDeepLinkingRequest message, which will redirect to the + * tool's content selection interface (e.g., /lti/select-media/). + * + * @param {HTMLElement} root - Modal root element + * @param {Object} ltiConfig - LTI configuration + */ + loadIframeLibraryViaLti(root, ltiConfig) { + // eslint-disable-next-line no-console + console.log('loadIframeLibraryViaLti called, config:', ltiConfig); + + const form = root.querySelector(Selectors.IFRAME.elements.form); + const pane = form.querySelector( + Selectors.IFRAME.elements.paneIframeLibrary, + ); + + if (!pane) { + // eslint-disable-next-line no-console + console.log('paneIframeLibrary not found!'); + return; + } + + const placeholderEl = pane.querySelector( + Selectors.IFRAME.elements.iframeLibraryPlaceholder, + ); + const loadingEl = pane.querySelector( + Selectors.IFRAME.elements.iframeLibraryLoading, + ); + const iframeEl = pane.querySelector( + Selectors.IFRAME.elements.iframeLibraryFrame, + ); + + if (!iframeEl) { + // eslint-disable-next-line no-console + console.log('iframeEl not found!'); + return; + } + + // Hide placeholder, show loading state + if (placeholderEl) { + placeholderEl.classList.add('d-none'); + } + if (loadingEl) { + loadingEl.classList.remove('d-none'); + } + iframeEl.classList.add('d-none'); + + // Set up load listener - note: this may fire multiple times during LTI redirects + const loadHandler = () => { + // eslint-disable-next-line no-console + console.log('LTI iframe loaded'); + this.handleIframeLibraryLoad(root); + }; + iframeEl.addEventListener('load', loadHandler); + + // Set the iframe src to the content item URL + // This initiates the LTI Deep Linking flow: + // 1. contentitem.php initiates OIDC login + // 2. LTI provider authenticates + // 3. Moodle sends LtiDeepLinkingRequest + // 4. Tool provider shows content selection interface (e.g., /lti/select-media/) + // eslint-disable-next-line no-console + console.log('Setting iframe src to LTI content item URL:', ltiConfig.contentItemUrl); + iframeEl.src = ltiConfig.contentItemUrl; + } + + /** + * Load the iframe library using static URL (fallback). + * + * @param {HTMLElement} root - Modal root element + */ + loadIframeLibraryStatic(root) { + // eslint-disable-next-line no-console + console.log( + 'loadIframeLibraryStatic called, URL:', + this.iframeLibraryUrl, + ); + + const form = root.querySelector(Selectors.IFRAME.elements.form); + const pane = form.querySelector( + Selectors.IFRAME.elements.paneIframeLibrary, + ); + + if (!pane) { + // eslint-disable-next-line no-console + console.log('paneIframeLibrary not found!'); + return; + } + + const placeholderEl = pane.querySelector( + Selectors.IFRAME.elements.iframeLibraryPlaceholder, + ); + const loadingEl = pane.querySelector( + Selectors.IFRAME.elements.iframeLibraryLoading, + ); + const iframeEl = pane.querySelector( + Selectors.IFRAME.elements.iframeLibraryFrame, + ); + + // eslint-disable-next-line no-console + console.log('Elements found:', { placeholderEl, loadingEl, iframeEl }); + + if (!iframeEl) { + // eslint-disable-next-line no-console + console.log('iframeEl not found!'); + return; + } + + // Hide placeholder, show loading state + if (placeholderEl) { + placeholderEl.classList.add('d-none'); + } + if (loadingEl) { + loadingEl.classList.remove('d-none'); + } + iframeEl.classList.add('d-none'); + + // Set up load listener before setting src + const loadHandler = () => { + // eslint-disable-next-line no-console + console.log('iframe loaded, src:', iframeEl.src); + // Only handle if the src matches our target URL + if (iframeEl.src === this.iframeLibraryUrl) { + this.handleIframeLibraryLoad(root); + // Remove the listener after successful load + iframeEl.removeEventListener('load', loadHandler); + } + }; + iframeEl.addEventListener('load', loadHandler); + + // Set the iframe source + iframeEl.src = this.iframeLibraryUrl; + // eslint-disable-next-line no-console + console.log('iframe src set to:', iframeEl.src); + } + + /** + * Handle iframe library load event. + * + * @param {HTMLElement} root - Modal root element + */ + handleIframeLibraryLoad(root) { + // eslint-disable-next-line no-console + console.log('handleIframeLibraryLoad called'); + + const form = root.querySelector(Selectors.IFRAME.elements.form); + const pane = form.querySelector( + Selectors.IFRAME.elements.paneIframeLibrary, + ); + + if (!pane) { + return; + } + + const placeholderEl = pane.querySelector( + Selectors.IFRAME.elements.iframeLibraryPlaceholder, + ); + const loadingEl = pane.querySelector( + Selectors.IFRAME.elements.iframeLibraryLoading, + ); + const iframeEl = pane.querySelector( + Selectors.IFRAME.elements.iframeLibraryFrame, + ); + + // Hide placeholder and loading, show iframe + if (placeholderEl) { + placeholderEl.classList.add('d-none'); + } + if (loadingEl) { + loadingEl.classList.add('d-none'); + } + if (iframeEl) { + iframeEl.classList.remove('d-none'); + } + + this.iframeLibraryLoaded = true; + } + + /** + * Handle messages from the iframe library (for video selection). + * Supports both custom videoSelected messages and LTI Deep Linking responses. + * + * @param {HTMLElement} root - Modal root element + * @param {MessageEvent} event - The message event + */ + handleIframeLibraryMessage(root, event) { + // eslint-disable-next-line no-console + console.log( + 'handleIframeLibraryMessage received:', + event.data, + 'from origin:', + event.origin, + ); + + const data = event.data; + + if (!data) { + return; + } + + // Handle custom videoSelected message format (from static iframe or custom MediaCMS implementation) + if (data.type === 'videoSelected' && data.embedUrl) { + // eslint-disable-next-line no-console + console.log( + 'Video selected (videoSelected):', + data.embedUrl, + 'videoId:', + data.videoId, + ); + this.selectIframeLibraryVideo(root, data.embedUrl, data.videoId); + return; + } + + // Handle LTI Deep Linking response format + if ( + data.type === 'ltiDeepLinkingResponse' || + data.messageType === 'LtiDeepLinkingResponse' + ) { + // eslint-disable-next-line no-console + console.log('LTI Deep Linking response received:', data); + const contentItems = data.content_items || data.contentItems || []; + if (contentItems.length > 0) { + const item = contentItems[0]; + // Extract embed URL from the content item + const embedUrl = + item.url || item.embed_url || item.embedUrl || ''; + const videoId = item.id || item.mediaId || ''; + if (embedUrl) { + // eslint-disable-next-line no-console + console.log( + 'Video selected (LTI):', + embedUrl, + 'videoId:', + videoId, + ); + this.selectIframeLibraryVideo(root, embedUrl, videoId); + } + } + return; + } + + // Handle MediaCMS specific message format (if different from above) + if (data.action === 'selectMedia' || data.action === 'mediaSelected') { + const embedUrl = data.embedUrl || data.url || ''; + const videoId = data.mediaId || data.videoId || data.id || ''; + if (embedUrl) { + // eslint-disable-next-line no-console + console.log( + 'Video selected (mediaSelected):', + embedUrl, + 'videoId:', + videoId, + ); + this.selectIframeLibraryVideo(root, embedUrl, videoId); + } + return; + } + } + + /** + * Select a video from the iframe library and populate the URL field. + * + * @param {HTMLElement} root - Modal root element + * @param {string} embedUrl - The embed URL for the video + * @param {string} videoId - The video ID + */ + selectIframeLibraryVideo(root, embedUrl, videoId) { + const form = root.querySelector(Selectors.IFRAME.elements.form); + + // Populate the URL field + const urlInput = form.querySelector(Selectors.IFRAME.elements.url); + urlInput.value = embedUrl; + + // Switch to the URL tab using our method + this.switchToUrlTab(root); + + // Update the preview + this.updatePreview(root); + + // Store the selected video + this.selectedLibraryVideo = { embedUrl, videoId }; + } +} diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/iframemodal.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/iframemodal.js new file mode 100755 index 00000000..f0fc1486 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/iframemodal.js @@ -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 . + +/** + * 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); + } +} diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/image.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/image.js new file mode 100755 index 00000000..e3519d4c --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/image.js @@ -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 . + +/** + * Tiny Media plugin Image class for Moodle. + * + * @module tiny_mediacms/image + * @copyright 2022 Huong Nguyen + * @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} + */ + 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} + */ + 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); + } +} diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/imagedetails.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/imagedetails.js new file mode 100755 index 00000000..adf35793 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/imagedetails.js @@ -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 . + +/** + * Tiny media plugin image details class for Moodle. + * + * @module tiny_mediacms/imagedetails + * @copyright 2024 Meirza + * @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, + }; + }; +} diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/imagehelpers.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/imagehelpers.js new file mode 100755 index 00000000..a59367b9 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/imagehelpers.js @@ -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 . + +/** + * Tiny media plugin image helpers. + * + * @module tiny_mediacms/imagehelpers + * @copyright 2024 Meirza + * @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} + */ +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} + */ +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} + */ +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} + */ +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+%/); +}; diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/imageinsert.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/imageinsert.js new file mode 100755 index 00000000..5941bc0e --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/imageinsert.js @@ -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 . + +/** + * Tiny media plugin image insertion class for Moodle. + * + * @module tiny_mediacms/imageinsert + * @copyright 2024 Meirza + * @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} 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); + }); + } + } +} \ No newline at end of file diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/imagemodal.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/imagemodal.js new file mode 100755 index 00000000..0f38b6d8 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/imagemodal.js @@ -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 . + +/** + * Image Modal for Tiny. + * + * @module tiny_mediacms/imagemodal + * @copyright 2022 Huong Nguyen + * @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(); diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/manager.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/manager.js new file mode 100755 index 00000000..0f4c5f4f --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/manager.js @@ -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 . + +/** + * Tiny Media Manager plugin class for Moodle. + * + * @module tiny_mediacms/manager + * @copyright 2022, Stevani Andolo + * @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(); + } +} diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/options.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/options.js new file mode 100755 index 00000000..7c5446f7 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/options.js @@ -0,0 +1,117 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Options helper for Tiny Media plugin. + * + * @module tiny_mediacms/options + * @copyright 2022 Huong Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {getPluginOptionName} from 'editor_tiny/options'; +import {pluginName} from './common'; + +const dataName = getPluginOptionName(pluginName, 'data'); +const permissionsName = getPluginOptionName(pluginName, 'permissions'); +const ltiName = getPluginOptionName(pluginName, 'lti'); + +/** + * Register the options for the Tiny Media plugin. + * + * @param {TinyMCE} editor + */ +export const register = (editor) => { + const registerOption = editor.options.register; + + registerOption(permissionsName, { + processor: 'object', + "default": { + image: { + filepicker: false, + } + }, + }); + + registerOption(dataName, { + processor: 'object', + "default": { + // MediaCMS video library configuration + mediacmsApiUrl: '', // e.g., 'https://deic.mediacms.io/api/v1/media' + mediacmsBaseUrl: '', // e.g., 'https://deic.mediacms.io' + mediacmsPageSize: 12, + // Auto-conversion settings + autoConvertEnabled: true, // Enable/disable auto-conversion of pasted MediaCMS URLs + autoConvertBaseUrl: '', // Base URL to restrict auto-conversion (empty = allow all MediaCMS domains) + autoConvertOptions: { + // Default embed options for auto-converted videos + showTitle: true, + linkTitle: true, + showRelated: true, + showUserAvatar: true, + }, + }, + }); + + registerOption(ltiName, { + processor: 'object', + "default": { + // LTI configuration for MediaCMS iframe library + toolId: 0, // LTI external tool ID + courseId: 0, // Current course ID + contentItemUrl: '', // URL to /mod/lti/contentitem.php for Deep Linking + }, + }); +}; + +/** + * Get the permissions configuration for the Tiny Media plugin. + * + * @param {TinyMCE} editor + * @returns {object} + */ +export const getPermissions = (editor) => editor.options.get(permissionsName); + +/** + * Get the permissions configuration for the Tiny Media plugin. + * + * @param {TinyMCE} editor + * @returns {object} + */ +export const getImagePermissions = (editor) => getPermissions(editor).image; + +/** + * Get the permissions configuration for the Tiny Media plugin. + * + * @param {TinyMCE} editor + * @returns {object} + */ +export const getEmbedPermissions = (editor) => getPermissions(editor).embed; + +/** + * Get the data configuration for the Media Manager. + * + * @param {TinyMCE} editor + * @returns {object} + */ +export const getData = (editor) => editor.options.get(dataName); + +/** + * Get the LTI configuration for the MediaCMS iframe library. + * + * @param {TinyMCE} editor + * @returns {object} + */ +export const getLti = (editor) => editor.options.get(ltiName); diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/plugin.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/plugin.js new file mode 100755 index 00000000..ab668f8b --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/plugin.js @@ -0,0 +1,92 @@ +// 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 . + +/** + * Tiny Media plugin for Moodle. + * + * @module tiny_mediacms/plugin + * @copyright 2022 Andrew Lyons + * @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'; + +// 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); + + // Clean up editor-only elements before content is saved. + // Remove wrapper divs and edit buttons that are only for the editor UI. + editor.on('GetContent', (e) => { + if (e.format === 'html') { + // Create a temporary container to manipulate the HTML + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = e.content; + + // Remove edit buttons + tempDiv.querySelectorAll('.tiny-mediacms-edit-btn').forEach(btn => btn.remove()); + + // Unwrap iframes from tiny-mediacms-iframe-wrapper + tempDiv.querySelectorAll('.tiny-mediacms-iframe-wrapper').forEach(wrapper => { + const iframe = wrapper.querySelector('iframe'); + if (iframe) { + wrapper.parentNode.insertBefore(iframe, wrapper); + } + wrapper.remove(); + }); + + // Unwrap iframes from tiny-iframe-responsive + tempDiv.querySelectorAll('.tiny-iframe-responsive').forEach(wrapper => { + const iframe = wrapper.querySelector('iframe'); + if (iframe) { + wrapper.parentNode.insertBefore(iframe, wrapper); + } + wrapper.remove(); + }); + + e.content = tempDiv.innerHTML; + } + }); + + return pluginMetadata; + }); + + // Resolve the Media Plugin and include configuration. + resolve([`${component}/plugin`, Configuration]); +}); diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/selectors.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/selectors.js new file mode 100755 index 00000000..e5149c16 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/selectors.js @@ -0,0 +1,162 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Tiny Media plugin helper function to build queryable data selectors. + * + * @module tiny_mediacms/selectors + * @copyright 2022 Huong Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +export default { + IMAGE: { + actions: { + submit: '.tiny_imagecms_urlentrysubmit', + imageBrowser: '.openimagecmsbrowser', + addUrl: '.tiny_imagecms_addurl', + deleteImage: '.tiny_imagecms_deleteicon', + }, + elements: { + form: 'form.tiny_imagecms_form', + alignSettings: '.tiny_imagecms_button', + alt: '.tiny_imagecms_altentry', + altWarning: '.tiny_imagecms_altwarning', + height: '.tiny_imagecms_heightentry', + width: '.tiny_imagecms_widthentry', + url: '.tiny_imagecms_urlentry', + urlWarning: '.tiny_imagecms_urlwarning', + size: '.tiny_imagecms_size', + presentation: '.tiny_imagecms_presentation', + constrain: '.tiny_imagecms_constrain', + customStyle: '.tiny_imagecms_customstyle', + preview: '.tiny_imagecms_preview', + previewBox: '.tiny_imagecms_preview_box', + loaderIcon: '.tiny_imagecms_loader', + loaderIconContainer: '.tiny_imagecms_loader_container', + insertImage: '.tiny_imagecms_insert_image', + modalFooter: '.modal-footer', + dropzoneContainer: '.tiny_imagecms_dropzone_container', + fileInput: '#tiny_imagecms_fileinput', + fileNameLabel: '.tiny_imagecms_filename', + sizeOriginal: '.tiny_imagecms_sizeoriginal', + sizeCustom: '.tiny_imagecms_sizecustom', + properties: '.tiny_imagecms_properties', + }, + styles: { + responsive: 'img-fluid', + }, + }, + EMBED: { + actions: { + submit: '.tiny_mediacms_submit', + mediaBrowser: '.openmediacmsbrowser', + }, + elements: { + form: 'form.tiny_mediacms_form', + source: '.tiny_mediacms_source', + track: '.tiny_mediacms_track', + mediaSource: '.tiny_mediacms_media_source', + linkSource: '.tiny_mediacms_link_source', + linkSize: '.tiny_mediacms_link_size', + posterSource: '.tiny_mediacms_poster_source', + posterSize: '.tiny_mediacms_poster_size', + displayOptions: '.tiny_mediacms_display_options', + name: '.tiny_mediacms_name_entry', + title: '.tiny_mediacms_title_entry', + url: '.tiny_mediacms_url_entry', + width: '.tiny_mediacms_width_entry', + height: '.tiny_mediacms_height_entry', + trackSource: '.tiny_mediacms_track_source', + trackKind: '.tiny_mediacms_track_kind_entry', + trackLabel: '.tiny_mediacms_track_label_entry', + trackLang: '.tiny_mediacms_track_lang_entry', + trackDefault: '.tiny_mediacms_track_default', + mediaControl: '.tiny_mediacms_controls', + mediaAutoplay: '.tiny_mediacms_autoplay', + mediaMute: '.tiny_mediacms_mute', + mediaLoop: '.tiny_mediacms_loop', + advancedSettings: '.tiny_mediacms_advancedsettings', + linkTab: 'li[data-medium-type="link"]', + videoTab: 'li[data-medium-type="video"]', + audioTab: 'li[data-medium-type="audio"]', + linkPane: '.tab-pane[data-medium-type="link"]', + videoPane: '.tab-pane[data-medium-type="video"]', + audioPane: '.tab-pane[data-medium-type="audio"]', + trackSubtitlesTab: 'li[data-track-kind="subtitles"]', + trackCaptionsTab: 'li[data-track-kind="captions"]', + trackDescriptionsTab: 'li[data-track-kind="descriptions"]', + trackChaptersTab: 'li[data-track-kind="chapters"]', + trackMetadataTab: 'li[data-track-kind="metadata"]', + trackSubtitlesPane: '.tab-pane[data-track-kind="subtitles"]', + trackCaptionsPane: '.tab-pane[data-track-kind="captions"]', + trackDescriptionsPane: '.tab-pane[data-track-kind="descriptions"]', + trackChaptersPane: '.tab-pane[data-track-kind="chapters"]', + trackMetadataPane: '.tab-pane[data-track-kind="metadata"]', + }, + mediaTypes: { + link: 'LINK', + video: 'VIDEO', + audio: 'AUDIO', + }, + trackKinds: { + subtitles: 'SUBTITLES', + captions: 'CAPTIONS', + descriptions: 'DESCRIPTIONS', + chapters: 'CHAPTERS', + metadata: 'METADATA', + }, + }, + IFRAME: { + actions: { + remove: '[data-action="remove"]', + }, + elements: { + form: 'form.tiny_iframecms_form', + url: '.tiny_iframecms_url', + urlWarning: '.tiny_iframecms_url_warning', + showTitle: '.tiny_iframecms_showtitle', + linkTitle: '.tiny_iframecms_linktitle', + showRelated: '.tiny_iframecms_showrelated', + showUserAvatar: '.tiny_iframecms_showuseravatar', + responsive: '.tiny_iframecms_responsive', + startAt: '.tiny_iframecms_startat', + startAtEnabled: '.tiny_iframecms_startat_enabled', + aspectRatio: '.tiny_iframecms_aspectratio', + width: '.tiny_iframecms_width', + height: '.tiny_iframecms_height', + preview: '.tiny_iframecms_preview', + previewContainer: '.tiny_iframecms_preview_container', + // Tab elements + tabs: '.tiny_iframecms_tabs', + tabUrlBtn: '.tiny_iframecms_tab_url_btn', + tabIframeLibraryBtn: '.tiny_iframecms_tab_iframe_library_btn', + paneUrl: '.tiny_iframecms_pane_url', + paneIframeLibrary: '.tiny_iframecms_pane_iframe_library', + // Iframe library elements + iframeLibraryContainer: '.tiny_iframecms_iframe_library_container', + iframeLibraryPlaceholder: + '.tiny_iframecms_iframe_library_placeholder', + iframeLibraryLoading: '.tiny_iframecms_iframe_library_loading', + iframeLibraryFrame: '.tiny_iframecms_iframe_library_frame', + }, + aspectRatios: { + '16:9': { width: 560, height: 315 }, + '4:3': { width: 560, height: 420 }, + '1:1': { width: 400, height: 400 }, + custom: null, + }, + }, +}; diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/usedfiles.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/usedfiles.js new file mode 100755 index 00000000..1a418046 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/usedfiles.js @@ -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 . + +/** + * Tiny Media Manager usedfiles. + * + * @module tiny_mediacms/usedfiles + * @copyright 2022, Stevani Andolo + * @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, '\\$&') + "(?.+?)[\\?\"']", '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; +}; diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/classes/form/manage_files_form.php b/lms-plugins/mediacms-moodle/tiny/mediacms/classes/form/manage_files_form.php new file mode 100755 index 00000000..00d021a1 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/classes/form/manage_files_form.php @@ -0,0 +1,118 @@ +. + +/** + * Atto text editor manage files plugin form. + * + * @package tiny_mediacms + * @copyright 2022, Stevani Andolo + * @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 + * @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, + ]); + } +} diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/classes/plugininfo.php b/lms-plugins/mediacms-moodle/tiny/mediacms/classes/plugininfo.php new file mode 100755 index 00000000..32c6c9df --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/classes/plugininfo.php @@ -0,0 +1,226 @@ +. + +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 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class plugininfo extends plugin implements plugin_with_buttons, plugin_with_menuitems, plugin_with_configuration { + /** + * Whether the plugin is enabled + * + * @param context $context The context that the editor is used within + * @param array $options The options passed in when requesting the editor + * @param array $fpoptions The filepicker options passed in when requesting the editor + * @param editor $editor The editor instance in which the plugin is initialised + * @return boolean + */ + public static function is_enabled( + context $context, + array $options, + array $fpoptions, + ?editor $editor = null + ): bool { + // Disabled if: + // - Not logged in or guest. + // - Files are not allowed. + // - Only URL are supported. + $canhavefiles = !empty($options['maxfiles']); + $canhaveexternalfiles = !empty($options['return_types']) && ($options['return_types'] & FILE_EXTERNAL); + + return isloggedin() && !isguestuser() && ($canhavefiles || $canhaveexternalfiles); + } + + public static function get_available_buttons(): array { + return [ + 'tiny_mediacms/tiny_mediacms_image', + 'tiny_mediacms/tiny_mediacms_video', + 'tiny_mediacms/tiny_mediacms_iframe', + ]; + } + + public static function get_available_menuitems(): array { + return [ + 'tiny_mediacms/tiny_mediacms_image', + 'tiny_mediacms/tiny_mediacms_video', + 'tiny_mediacms/tiny_mediacms_iframe', + ]; + } + + public static function get_plugin_configuration_for_context( + context $context, + array $options, + array $fpoptions, + ?editor $editor = null + ): array { + + // TODO Fetch the actual permissions. + $permissions = [ + 'image' => [ + 'filepicker' => true, + ], + 'embed' => [ + 'filepicker' => true, + ] + ]; + + // Get LTI configuration for MediaCMS iframe library. + $lticonfig = self::get_lti_configuration($context); + + // Get auto-convert configuration. + $autoconvertconfig = self::get_autoconvert_configuration(); + + return array_merge([ + 'permissions' => $permissions, + ], self::get_file_manager_configuration($context, $options, $fpoptions), $lticonfig, $autoconvertconfig); + } + + /** + * Get the auto-convert configuration for pasted MediaCMS URLs. + * + * @return array Auto-convert configuration data + */ + protected static function get_autoconvert_configuration(): array { + $baseurl = get_config('tiny_mediacms', 'autoconvert_baseurl'); + + // Helper function to get config with default value of true. + $getboolconfig = function($name) { + $value = get_config('tiny_mediacms', $name); + // If the setting hasn't been saved yet (false/empty), default to true. + // Only return false if explicitly set to '0'. + return $value !== '0' && $value !== 0; + }; + + return [ + 'data' => [ + 'autoConvertEnabled' => true, // Always enabled + 'autoConvertBaseUrl' => !empty($baseurl) ? $baseurl : '', + 'autoConvertOptions' => [ + 'showTitle' => $getboolconfig('autoconvert_showtitle'), + 'linkTitle' => $getboolconfig('autoconvert_linktitle'), + 'showRelated' => $getboolconfig('autoconvert_showrelated'), + 'showUserAvatar' => $getboolconfig('autoconvert_showuseravatar'), + ], + ], + ]; + } + + /** + * Get the LTI configuration for the MediaCMS iframe library. + * + * @param context $context The context that the editor is used within + * @return array LTI configuration data + */ + protected static function get_lti_configuration(context $context): array { + global $COURSE; + + // Get the configured LTI tool ID from plugin settings. + $ltitoolid = get_config('tiny_mediacms', 'ltitoolid'); + + // Determine the course ID from context. + $courseid = 0; + if ($context instanceof context_course) { + $courseid = $context->instanceid; + } else if ($context instanceof context_module) { + // Get the course from the module context. + $coursecontext = $context->get_course_context(false); + if ($coursecontext) { + $courseid = $coursecontext->instanceid; + } + } else if (!empty($COURSE->id) && $COURSE->id != SITEID) { + // Fall back to the global $COURSE if available and not the site. + $courseid = $COURSE->id; + } + + // Build the content item URL for LTI Deep Linking. + // This URL initiates the LTI Deep Linking flow which allows users + // to select content (like videos) from the tool provider. + $contentitemurl = ''; + if (!empty($ltitoolid) && $courseid > 0) { + $contentitemurl = (new moodle_url('/mod/lti/contentitem.php', [ + 'id' => $ltitoolid, + 'course' => $courseid, + 'title' => 'MediaCMS Library', + 'return_types' => 1 // LTI_DEEPLINKING_RETURN_TYPE_LTI_LINK + ]))->out(false); + } + + return [ + 'lti' => [ + 'toolId' => !empty($ltitoolid) ? (int) $ltitoolid : 0, + 'courseId' => $courseid, + 'contentItemUrl' => $contentitemurl, + ], + ]; + } + + protected static function get_file_manager_configuration( + context $context, + array $options, + array $fpoptions + ): array { + global $USER; + + $params = [ + 'area' => [], + 'usercontext' => \context_user::instance($USER->id)->id, + ]; + + $keys = [ + 'itemid', + 'areamaxbytes', + 'maxbytes', + 'subdirs', + 'return_types', + 'removeorphaneddrafts', + ]; + if (isset($options['context'])) { + if (is_object($options['context'])) { + $params['area']['context'] = $options['context']->id; + } else { + $params['area']['context'] = $options['context']; + } + } + foreach ($keys as $key) { + if (isset($options[$key])) { + $params['area'][$key] = $options[$key]; + } + } + + return [ + 'storeinrepo' => true, + 'data' => [ + 'params' => $params, + 'fpoptions' => $fpoptions, + ], + ]; + } +} diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/classes/privacy/provider.php b/lms-plugins/mediacms-moodle/tiny/mediacms/classes/privacy/provider.php new file mode 100755 index 00000000..14eeaf6f --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/classes/privacy/provider.php @@ -0,0 +1,30 @@ +. + +namespace tiny_mediacms\privacy; + +/** + * Privacy Subsystem implementation for the media plugin for TinyMCE. + * + * @package tiny_media + * @copyright 2022 Andrew Lyons + * @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'; + } +} diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/lang/en/deprecated.txt b/lms-plugins/mediacms-moodle/tiny/mediacms/lang/en/deprecated.txt new file mode 100755 index 00000000..431de560 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/lang/en/deprecated.txt @@ -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 diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/lang/en/tiny_mediacms.php b/lms-plugins/mediacms-moodle/tiny/mediacms/lang/en/tiny_mediacms.php new file mode 100755 index 00000000..0463f9eb --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/lang/en/tiny_mediacms.php @@ -0,0 +1,199 @@ +. + +/** + * Strings for component 'tiny_mediacms', language 'en'. + * + * @package tiny_mediacms + * @copyright 2022 Andrew Lyons + * @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
click the Browse Repositories button.'; +$string['saveimage'] = 'Save'; +$string['size'] = 'Width x height (in pixels)'; +$string['sizecustom'] = 'Custom size'; +$string['sizecustom_help'] = 'This image is just a preview. Changes to its size will be visible after you save it.'; +$string['sizeoriginal'] = 'Original size'; +$string['srclang'] = 'Language'; +$string['subtitles_help'] = 'Subtitles may be used to provide a transcription or translation of the dialogue.'; +$string['subtitles'] = 'Subtitles'; +$string['subtitlessourcelabel'] = 'Subtitle track URL'; +$string['tracks_help'] = 'Subtitles, captions, chapters and descriptions can be added via a WebVTT (Web Video Text Tracks) format file. Track labels will be shown in the selection drop-down menu. For each type of track, any track set as default will be pre-selected at the start of the video.'; +$string['tracks'] = 'Subtitles and captions'; +$string['unusedfilesdesc'] = 'The following embedded files are not used in the text area:'; +$string['unusedfilesheader'] = 'Unused files'; +$string['unusedfilesremovalnotice'] = 'Any unused files will be automatically deleted when saving changes.'; +$string['updatemedia'] = 'Update favorite media'; +$string['uploading'] = 'Uploading'; +$string['video'] = 'Video'; +$string['videoheight'] = 'Video height'; +$string['videosourcelabel'] = 'Video source URL'; +$string['videowidth'] = 'Video width'; +$string['width'] = 'Width'; + +// Iframe embed strings. +$string['iframebuttontitle'] = 'Insert MediaCMS Media'; +$string['iframemodaltitle'] = 'Insert MediaCMS Media'; +$string['iframeurl'] = 'MediaCMS Video URL or embed code'; +$string['iframeurlplaceholder'] = 'Paste MediaCMS Video URL or iframe embed code'; +$string['iframeurlinvalid'] = 'Please enter a valid MediaCMS Video URL or embed code'; +$string['embedoptions'] = 'Embed Options'; +$string['showtitle'] = 'Show title'; +$string['linktitle'] = 'Link title'; +$string['showrelated'] = 'Show related'; +$string['showuseravatar'] = 'Show user avatar'; +$string['responsive'] = 'Responsive'; +$string['startat'] = 'Start at'; +$string['aspectratio'] = 'Aspect Ratio'; +$string['aspectratio_16_9'] = '16:9'; +$string['aspectratio_4_3'] = '4:3'; +$string['aspectratio_1_1'] = '1:1'; +$string['aspectratio_custom'] = 'Custom'; +$string['dimensions'] = 'Dimensions'; +$string['preview'] = 'Preview'; +$string['insertiframe'] = 'Insert video'; +$string['updateiframe'] = 'Update video'; +$string['removeiframe'] = 'Remove video'; +$string['removeiframeconfirm'] = 'Are you sure you want to remove this video from the editor?'; + +// Iframe modal tabs. +$string['tabembedurl'] = 'Embed URL'; +$string['tabvideolibrary'] = 'Video Library'; +$string['tabvideolibraryiframe'] = 'Media Library'; + +// Video library strings. +$string['librarysearchplaceholder'] = 'Search videos...'; +$string['librarysortnewest'] = 'Newest first'; +$string['librarysortoldest'] = 'Oldest first'; +$string['librarysorttitle'] = 'Title A-Z'; +$string['librarysortviews'] = 'Most views'; +$string['libraryloading'] = 'Loading videos...'; +$string['libraryempty'] = 'No videos found'; +$string['libraryerror'] = 'Failed to load videos'; +$string['libraryretry'] = 'Retry'; +$string['libraryprev'] = 'Previous'; +$string['librarynext'] = 'Next'; +$string['libraryselect'] = 'Select'; +$string['librarypage'] = 'Page {$a->current} of {$a->total}'; +$string['libraryvideoselected'] = 'Video selected. Configure embed options below.'; + +// LTI settings strings. +$string['ltitoolid'] = 'LTI Tool'; +$string['ltitoolid_desc'] = 'Select the External Tool configuration for MediaCMS. This enables the authenticated video library in the editor.'; +$string['noltitoolsfound'] = 'No LTI tools found'; +$string['choose'] = 'Choose...'; +$string['ltitoolid_help'] = 'To find the LTI tool ID, go to Site administration > Plugins > Activity modules > External tool > Manage tools. The ID is shown in the URL when editing a tool (e.g., id=2).'; + +// Iframe library from LTI strings. +$string['iframelibraryloading'] = 'Loading MediaCMS video library...'; +$string['iframelibraryplaceholder'] = 'Click here to load the video library'; +$string['iframelibraryerror'] = 'Failed to load the video library. Please check your LTI configuration.'; +$string['iframelibrarynotconfigured'] = 'The MediaCMS LTI tool has not been configured. Please contact your administrator.'; + +// Auto-convert settings strings. +$string['autoconvertheading'] = 'Auto-convert MediaCMS URLs'; +$string['autoconvertheading_desc'] = 'Configure automatic conversion of pasted MediaCMS URLs to embedded videos.'; +$string['autoconvertenabled'] = 'Enable auto-convert'; +$string['autoconvertenabled_desc'] = 'When enabled, pasting a MediaCMS video URL (e.g., https://lti.mediacms.io/view?m=VIDEO_ID) into the editor will automatically convert it to an embedded video player.'; +$string['autoconvert_baseurl'] = 'MediaCMS URL'; +$string['autoconvert_baseurl_desc'] = 'The base URL of your MediaCMS instance (e.g., https://lti.mediacms.io). If specified, only URLs from this domain will be auto-converted. Leave empty to allow any MediaCMS URL.'; +$string['autoconvert_showtitle'] = 'Show video title'; +$string['autoconvert_showtitle_desc'] = 'Display the video title in the embedded player.'; +$string['autoconvert_linktitle'] = 'Link video title'; +$string['autoconvert_linktitle_desc'] = 'Make the video title clickable, linking to the original video page.'; +$string['autoconvert_showrelated'] = 'Show related videos'; +$string['autoconvert_showrelated_desc'] = 'Display related videos after the current video ends.'; +$string['autoconvert_showuseravatar'] = 'Show user avatar'; +$string['autoconvert_showuseravatar_desc'] = 'Display the uploader\'s avatar in the embedded player.'; + +// Deprecated since Moodle 4.4. +$string['alignment'] = 'Alignment'; +$string['alignment_bottom'] = 'Bottom'; +$string['alignment_left'] = 'Left'; +$string['alignment_middle'] = 'Middle'; +$string['alignment_right'] = 'Right'; +$string['alignment_top'] = 'Top'; + +// Deprecated since Moodle 4.5. +$string['helplinktext'] = 'Favorite media helper'; diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/manage.php b/lms-plugins/mediacms-moodle/tiny/mediacms/manage.php new file mode 100755 index 00000000..844953cc --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/manage.php @@ -0,0 +1,148 @@ +. + +/** + * Manage files in user draft area attached to texteditor. + * + * @package tiny_mediacms + * @copyright 2022, Stevani Andolo + * @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(); diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/pix/filemanager.svg b/lms-plugins/mediacms-moodle/tiny/mediacms/pix/filemanager.svg new file mode 100755 index 00000000..a210b89a --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/pix/filemanager.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/pix/icon.svg b/lms-plugins/mediacms-moodle/tiny/mediacms/pix/icon.svg new file mode 100755 index 00000000..6ee4edd2 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/pix/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/settings.php b/lms-plugins/mediacms-moodle/tiny/mediacms/settings.php new file mode 100755 index 00000000..5eb78f5b --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/settings.php @@ -0,0 +1,97 @@ +. + +/** + * Settings for the tiny_mediacms plugin. + * + * @package tiny_mediacms + * @copyright 2024 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +if ($ADMIN->fulltree) { + global $DB; + + // LTI Tool ID setting (Dropdown). + $ltioptions = [0 => get_string('noltitoolsfound', 'tiny_mediacms')]; + try { + $tools = $DB->get_records('lti_types', null, 'name ASC', 'id, name, baseurl'); + if (!empty($tools)) { + $ltioptions = [0 => get_string('choose', 'tiny_mediacms')]; + foreach ($tools as $tool) { + $ltioptions[$tool->id] = $tool->name . ' (' . $tool->baseurl . ')'; + } + } + } catch (Exception $e) { + // Database might not be ready during install + } + + $setting = new admin_setting_configselect( + 'tiny_mediacms/ltitoolid', + new lang_string('ltitoolid', 'tiny_mediacms'), + new lang_string('ltitoolid_desc', 'tiny_mediacms'), + 0, + $ltioptions + ); + $settings->add($setting); + + // Auto-convert is enabled by default in plugininfo.php (data.autoConvertEnabled = true). + + // MediaCMS base URL for auto-convert. + $setting = new admin_setting_configtext( + 'tiny_mediacms/autoconvert_baseurl', + new lang_string('autoconvert_baseurl', 'tiny_mediacms'), + new lang_string('autoconvert_baseurl_desc', 'tiny_mediacms'), + 'https://lti.mediacms.io', // Default matching filter + PARAM_URL + ); + $settings->add($setting); + + // Auto-convert embed options. + $setting = new admin_setting_configcheckbox( + 'tiny_mediacms/autoconvert_showtitle', + new lang_string('autoconvert_showtitle', 'tiny_mediacms'), + new lang_string('autoconvert_showtitle_desc', 'tiny_mediacms'), + 1 + ); + $settings->add($setting); + + $setting = new admin_setting_configcheckbox( + 'tiny_mediacms/autoconvert_linktitle', + new lang_string('autoconvert_linktitle', 'tiny_mediacms'), + new lang_string('autoconvert_linktitle_desc', 'tiny_mediacms'), + 1 + ); + $settings->add($setting); + + $setting = new admin_setting_configcheckbox( + 'tiny_mediacms/autoconvert_showrelated', + new lang_string('autoconvert_showrelated', 'tiny_mediacms'), + new lang_string('autoconvert_showrelated_desc', 'tiny_mediacms'), + 1 + ); + $settings->add($setting); + + $setting = new admin_setting_configcheckbox( + 'tiny_mediacms/autoconvert_showuseravatar', + new lang_string('autoconvert_showuseravatar', 'tiny_mediacms'), + new lang_string('autoconvert_showuseravatar_desc', 'tiny_mediacms'), + 1 + ); + $settings->add($setting); +} diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/styles.css b/lms-plugins/mediacms-moodle/tiny/mediacms/styles.css new file mode 100755 index 00000000..211dea40 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/styles.css @@ -0,0 +1,79 @@ +#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; +} + + +@media (max-width: 767px) { + .tiny_imagecms_form .tiny_imagecms_properties_col { + padding: 0; + } +} diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/templates/embed_media_audio.mustache b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/embed_media_audio.mustache new file mode 100755 index 00000000..8e596063 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/embed_media_audio.mustache @@ -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 . +}} +{{! + @template tiny_mediacms/embed_media_audio + + Embed media audio template. + + Example context (json): + { + + } +}} +  + {{#sources}}{{/sources}} + {{#tracks}} + + {{/tracks}} + {{#description}}{{.}}{{/description}} +  diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/templates/embed_media_link.mustache b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/embed_media_link.mustache new file mode 100755 index 00000000..d87538a6 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/embed_media_link.mustache @@ -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 . +}} +{{! + @template tiny_mediacms/embed_media_link + + Embed media link template. + + Example context (json): + { + + } +}} +{{! + }}{{#name}}{{.}}{{/name}}{{! + }}{{^name}}{{url}}{{/name}}{{! +}} diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/templates/embed_media_modal.mustache b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/embed_media_modal.mustache new file mode 100755 index 00000000..6ae910f5 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/embed_media_modal.mustache @@ -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 . +}} +{{! + @template tiny_mediacms/embed_media_modal + + Embed media modal template. + + Example context (json): + { + + } +}} +{{< core/modal }} + + {{$title}} + {{#str}} modaltitle, tiny_h5p {{/str}} + {{/title}} + + {{$body}} +
+ +
+ +
+ {{> tiny_mediacms/embed_media_modal_video}} +
+
+ {{> tiny_mediacms/embed_media_modal_audio}} +
+
+
+ {{/body}} + + {{$footer}} + + + {{/footer}} + +{{/ core/modal }} diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/templates/embed_media_modal_audio.mustache b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/embed_media_modal_audio.mustache new file mode 100755 index 00000000..2c350cec --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/embed_media_modal_audio.mustache @@ -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 . +}} +{{! + @template tiny_mediacms/embed_media_modal_audio + + Embed media audio modal template. + + Example context (json): + { + + } +}} +{{#audio.sources}} +
+
+ +
+ + {{#showfilepicker}} + + + + {{/showfilepicker}} +
+
+
+ + {{#str}} addsource, tiny_mediacms {{/str}} + + {{#addsourcehelpicon}} + {{> core/help_icon }} + {{/addsourcehelpicon}} +
+ +
+{{/audio.sources}} +{{^audio}} +
+
+ +
+ + {{#showfilepicker}} + + + + {{/showfilepicker}} +
+
+
+ + {{#str}} addsource, tiny_mediacms {{/str}} + + {{#addsourcehelpicon}} + {{> core/help_icon }} + {{/addsourcehelpicon}} +
+ +
+{{/audio}} + + + diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/templates/embed_media_modal_link.mustache b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/embed_media_modal_link.mustache new file mode 100755 index 00000000..b5fe580b --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/embed_media_modal_link.mustache @@ -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 . +}} +{{! + @template tiny_mediacms/embed_media_modal_link + + Embed media link modal template. + + Example context (json): + { + + } +}} +
+
+ +
+ + {{#showfilepicker}} + + + + {{/showfilepicker}} +
+
+
+ + diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/templates/embed_media_modal_video.mustache b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/embed_media_modal_video.mustache new file mode 100755 index 00000000..0aa73e3f --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/embed_media_modal_video.mustache @@ -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 . +}} +{{! + @template tiny_mediacms/embed_media_modal_video + + Embed media video modal template. + + Example context (json): + { + + } +}} +{{#video.sources}} +
+
+ +
+ + {{#showfilepicker}} + + + + {{/showfilepicker}} +
+
+
+ + {{#str}} addsource, tiny_mediacms {{/str}} + + {{#addsourcehelpicon}} + {{> core/help_icon }} + {{/addsourcehelpicon}} +
+ +
+{{/video.sources}} +{{^video}} +
+
+ +
+ + {{#showfilepicker}} + + + + {{/showfilepicker}} +
+
+
+ + {{#str}} addsource, tiny_mediacms {{/str}} + + {{#addsourcehelpicon}} + {{> core/help_icon }} + {{/addsourcehelpicon}} +
+ +
+{{/video}} + + + diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/templates/embed_media_video.mustache b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/embed_media_video.mustache new file mode 100755 index 00000000..756c3e8f --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/embed_media_video.mustache @@ -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 . +}} +{{! + @template tiny_mediacms/embed_media_video + + Embed media video template. + + Example context (json): + { + + } +}} +  + {{#sources}} + + {{/sources}} + {{#tracks}} + + {{/tracks}} + {{#description}}{{.}}{{/description}} +  diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/templates/iframe_embed_modal.mustache b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/iframe_embed_modal.mustache new file mode 100755 index 00000000..6726bc99 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/iframe_embed_modal.mustache @@ -0,0 +1,134 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tiny_mediacms/iframe_embed_modal + + Iframe embed modal template. + + Example context (json): + { + "elementid": "editor1", + "isupdating": false + } +}} +{{< core/modal }} + + {{$body}} +
+ + + + +
+ +
+
+
+ +
+ +
+ + +
+ {{#str}} iframeurlinvalid, tiny_mediacms {{/str}} +
+
+ + {{> tiny_mediacms/iframe_embed_options }} +
+ + +
+ +
+
+ {{#str}} iframeurlplaceholder, tiny_mediacms {{/str}} +
+
+
+
+
+
+ + +
+
+
+

{{#str}} libraryloading, tiny_mediacms {{/str}}

+
+
+
+ {{#str}} libraryloading, tiny_mediacms {{/str}} +
+

{{#str}} libraryloading, tiny_mediacms {{/str}}

+
+ +
+
+
+
+ {{/body}} + + {{$footer}} + + {{#isupdating}} + + {{/isupdating}} + + {{/footer}} + +{{/ core/modal }} diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/templates/iframe_embed_options.mustache b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/iframe_embed_options.mustache new file mode 100755 index 00000000..7ea36182 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/iframe_embed_options.mustache @@ -0,0 +1,127 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tiny_mediacms/iframe_embed_options + + Embed options partial for iframe modal. + + Example context (json): + { + "elementid": "editor1", + "showTitle": true, + "linkTitle": true, + "showRelated": true, + "showUserAvatar": true, + "responsive": true, + "startAtEnabled": false, + "startAt": "0:00" + } +}} + +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + + +
+
+
+
+ + +
+ + +
+ + +
+ +
+
+
+ + px +
+ {{#str}} width, tiny_mediacms {{/str}} +
+
+
+ + px +
+ {{#str}} height, tiny_mediacms {{/str}} +
+
+
diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/templates/iframe_embed_output.mustache b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/iframe_embed_output.mustache new file mode 100755 index 00000000..1c9edfad --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/iframe_embed_output.mustache @@ -0,0 +1,36 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tiny_mediacms/iframe_embed_output + + Iframe embed output template. + + Example context (json): + { + "src": "https://example.com/embed?m=abc123", + "width": 560, + "height": 315, + "responsive": true, + "aspectRatioClass": "ratio-16-9" + } +}} +{{#responsive}} + +{{/responsive}} +{{^responsive}} + +{{/responsive}} diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/templates/iframe_video_library.mustache b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/iframe_video_library.mustache new file mode 100755 index 00000000..404af4dd --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/iframe_video_library.mustache @@ -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 . +}} +{{! + @template tiny_mediacms/iframe_video_library + + Video library browser for iframe modal. + + Example context (json): + { + "elementid": "editor1" + } +}} +
+ +
+
+
+ + +
+
+
+ +
+
+ + +
+ +
+
+ {{#str}} loading, tiny_mediacms {{/str}} +
+

{{#str}} libraryloading, tiny_mediacms {{/str}}

+
+ + +
+ +
+ + +
+ +

{{#str}} libraryempty, tiny_mediacms {{/str}}

+
+ + +
+ +

{{#str}} libraryerror, tiny_mediacms {{/str}}

+ +
+
+ + +
+ + + +
+
diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/templates/iframe_video_library_item.mustache b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/iframe_video_library_item.mustache new file mode 100755 index 00000000..6cb41e30 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/iframe_video_library_item.mustache @@ -0,0 +1,73 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tiny_mediacms/iframe_video_library_item + + Single video item in the library grid. + + Example context (json): + { + "id": "abc123", + "title": "My Video", + "thumbnail": "https://example.com/thumb.jpg", + "duration": "2:30", + "views": "150 views", + "date": "2 weeks ago", + "embedUrl": "https://example.com/embed?m=abc123" + } +}} +
+
+
+ {{title}} + {{#duration}} + + {{duration}} + + {{/duration}} + +
+ +
+
+
+
+ {{title}} +
+

+ {{#views}}{{views}} • {{/views}}{{date}} +

+
+
+
+ diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/templates/image.mustache b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/image.mustache new file mode 100755 index 00000000..99be5816 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/image.mustache @@ -0,0 +1,34 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tiny_mediacms/image + + Image template. + + Example context (json): + { + + } +}} +{{alt}} diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/templates/insert_image_modal.mustache b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/insert_image_modal.mustache new file mode 100755 index 00000000..bf5a78d2 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/insert_image_modal.mustache @@ -0,0 +1,61 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tiny_mediacms/insert_image_modal + + Insert image template. + + Example context (json): + { + "uniqid": 0, + "elementid": "exampleId", + "loading": {}, + "title": "Insert image" + } +}} +{{< core/modal }} + {{$body}} +
+ + + + + +
+
+ + {{> core/loading }} + +
{{#str}} loading, tiny_mediacms {{/str}}
+
+
+
+
+ {{/body}} + + {{$footer}} + + {{/footer}} +{{/ core/modal }} diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/templates/insert_image_modal_details.mustache b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/insert_image_modal_details.mustache new file mode 100755 index 00000000..36d4f608 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/insert_image_modal_details.mustache @@ -0,0 +1,112 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tiny_mediacms/insert_image_modal_details + + Insert image details body template. + + Example context (json): + { + "elementid": "exampleId", + "alt": "Image description", + "presentation": true, + "width": 600, + "height": 400, + "customStyle": "", + "sizecustomhelpicon": { + "text": "Help text" + } + } + +}} +
+
+
+ +
+ + +
+ +
+ +
+ + +
+ +
+ + + +
+ 0 + / 125 +
+
+
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ +
+
+ + +
+
+ +
X
+ +
+
+ + +
+
+
{{#sizecustomhelpicon}}{{> core/help_icon }}{{/sizecustomhelpicon}}
+
+
+ +
+ + +
+
+
+
+
+
diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/templates/insert_image_modal_details_footer.mustache b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/insert_image_modal_details_footer.mustache new file mode 100755 index 00000000..9fd5fd71 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/insert_image_modal_details_footer.mustache @@ -0,0 +1,42 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tiny_mediacms/insert_image_modal_details_footer + + Insert image details footer template. + + Example context (json): + { + "elementid": "exampleId" + } + +}} +
+ +
+ + +
+ +
+ + + + +
+
+ diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/templates/insert_image_modal_insert.mustache b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/insert_image_modal_insert.mustache new file mode 100755 index 00000000..8a0b091c --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/insert_image_modal_insert.mustache @@ -0,0 +1,53 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tiny_mediacms/insert_image_modal_insert + + Insert image body template. + + Example context (json): + { + "showdropzone": true, + "showfilepicker": true + } + +}} +
+
+ {{#showdropzone}} +
+ + + {{/showdropzone}} + {{^showdropzone}} +
+ + + +
+

+ {{#showfilepicker}} + {{#str}} repositoryuploadnotpermitted, tiny_mediacms {{/str}}

+ {{/showfilepicker}} + {{^showfilepicker}} + {{#str}} repositorynotpermitted, tiny_mediacms {{/str}} + {{/showfilepicker}} +
+
+ {{/showdropzone}} +
+
diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/templates/insert_image_modal_insert_footer.mustache b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/insert_image_modal_insert_footer.mustache new file mode 100755 index 00000000..bda98412 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/insert_image_modal_insert_footer.mustache @@ -0,0 +1,56 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tiny_mediacms/insert_image_modal_insert_footer + + Insert image footer template. + + Example context (json): + { + "showdropzone": true, + "elementid": "exampleId", + "src": "https://moodle.org/logo.png", + "showfilepicker": true + } + +}} +
+ +
+ + {{#showdropzone}} + + {{/showdropzone}} + {{^showdropzone}} + + {{/showdropzone}} + + + + +
+ +
+ + + {{#showfilepicker}} + + + {{/showfilepicker}} +
+
+ diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/templates/missingfiles.mustache b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/missingfiles.mustache new file mode 100755 index 00000000..cbde9013 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/missingfiles.mustache @@ -0,0 +1,34 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tiny_mediacms/missingfiles + + Insert image template. + + Example context (json): + { + "missingFiles": [ + "Example.png", + "Another.mov" + ] + } +}} +
    +{{#missingFiles}} +
  1. {{.}}
  2. +{{/missingFiles}} +
diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/templates/mm2_iframe.mustache b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/mm2_iframe.mustache new file mode 100755 index 00000000..5aee7a68 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/templates/mm2_iframe.mustache @@ -0,0 +1,28 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tiny_mediacms/mm2_iframe + + Insert image template. + + Example context (json): + { + + } +}} + diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/tests/behat/image.feature b/lms-plugins/mediacms-moodle/tiny/mediacms/tests/behat/image.feature new file mode 100755 index 00000000..1ec162c8 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/tests/behat/image.feature @@ -0,0 +1,74 @@ +@editor @editor_tiny @tiny_mediacms @javascript +Feature: Use the TinyMCE editor to upload an image + In order to work with images + As a user + I need to be able to upload and manipulate images + + Scenario: Clicking on the Image button in the TinyMCE editor opens the image dialog + Given I log in as "admin" + And I open my profile in edit mode + When I click on the "Image" button for the "Description" TinyMCE editor + Then "Insert image" "dialogue" should exist + + Scenario: Browsing repositories in the TinyMCE editor opens the image dialog and shows the FilePicker + Given I log in as "admin" + And I open my profile in edit mode + When I click on the "Image" button for the "Description" TinyMCE editor + And I click on "Browse repositories" "button" in the "Insert image" "dialogue" + Then "File picker" "dialogue" should exist + + Scenario: Focus returns to the correct location after closing a nested FilePicker + Given I log in as "admin" + And I open my profile in edit mode + When I click on the "Image" button for the "Description" TinyMCE editor + And I press "Browse repositories" + When I press the escape key + Then the focused element is "Browse repositories" "button" + + @_file_upload @test_tiny + Scenario: Browsing repositories in the TinyMCE editor shows the FilePicker and upload url image + Given I log in as "admin" + And I open my profile in edit mode + When I click on the "Image" button for the "Description" TinyMCE editor + And I click on "Browse repositories" "button" in the "Insert image" "dialogue" + And I upload "/lib/editor/tiny/tests/behat/fixtures/tinyscreenshot.png" to the file picker for TinyMCE + # Note: This needs to be replaced with a label. + Then ".tiny_image_preview" "css_element" should be visible + + @_file_upload + Scenario: Insert image to the TinyMCE editor + Given I log in as "admin" + And I open my profile in edit mode + And I click on the "Image" button for the "Description" TinyMCE editor + And I click on "Browse repositories" "button" in the "Insert image" "dialogue" + And I upload "lib/editor/tiny/tests/behat/fixtures/moodle-logo.png" to the file picker for TinyMCE + And I set the field "How would you describe this image to someone who can't see it?" to "It's the Moodle" + And I click on "Save" "button" in the "Image details" "dialogue" + When I select the "img" element in position "0" of the "Description" TinyMCE editor + And I click on the "Image" button for the "Description" TinyMCE editor + Then the field "How would you describe this image to someone who can't see it?" matches value "It's the Moodle" + # Note: This needs to be replaced with a label. + And ".tiny_image_preview" "css_element" should be visible + + @_file_upload + Scenario: Resizing the image uses the original and custom sizes and the keep proportion checkbox + Given I log in as "admin" + And I open my profile in edit mode + And I click on the "Image" button for the "Description" TinyMCE editor + And I click on "Browse repositories" "button" in the "Insert image" "dialogue" + And I upload "lib/editor/tiny/tests/behat/fixtures/moodle-logo.png" to the file picker for TinyMCE + And I click on "This image is decorative only" "checkbox" + And I click on "Save" "button" in the "Image details" "dialogue" + When I select the "img" element in position "0" of the "Description" TinyMCE editor + And I click on the "Image" button for the "Description" TinyMCE editor + Then the field "Original size" matches value "1" + And I click on "Custom size" "radio" + Then the field "Keep proportion" matches value "1" + And I click on "Keep proportion" "checkbox" + And I set the field "Width" to "102" + And I click on "Save" "button" in the "Image details" "dialogue" + When I select the "img" element in position "0" of the "Description" TinyMCE editor + And I click on the "Image" button for the "Description" TinyMCE editor + Then the field "Custom size" matches value "1" + And the field "Width" matches value "102" + And the field "Keep proportion" matches value "0" diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/tests/behat/video.feature b/lms-plugins/mediacms-moodle/tiny/mediacms/tests/behat/video.feature new file mode 100755 index 00000000..f8d72890 --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/tests/behat/video.feature @@ -0,0 +1,58 @@ +@editor @editor_tiny @tiny_mediacms @javascript +Feature: Use the TinyMCE editor to upload a video + In order to work with videos + As a user + I need to be able to upload and manipulate videos + + Scenario: Clicking on the Video button in the TinyMCE editor opens the video dialog + Given I log in as "admin" + And I open my profile in edit mode + When I click on the "Multimedia" button for the "Description" TinyMCE editor + Then "Insert media" "dialogue" should exist + + Scenario: Browsing repositories in the TinyMCE editor shows the FilePicker + Given I log in as "admin" + And I open my profile in edit mode + When I click on the "Multimedia" button for the "Description" TinyMCE editor + And I click on "Browse repositories" "button" in the "Insert media" "dialogue" + Then "File picker" "dialogue" should exist + + @_file_upload + Scenario: Browsing repositories in the TinyMCE editor shows the FilePicker + Given I log in as "admin" + And I open my profile in edit mode + When I click on the "Multimedia" button for the "Description" TinyMCE editor + And I follow "Video" + And I click on "Browse repositories..." "button" in the "#id_description_editor_video .tiny_media_source.tiny_media_media_source" "css_element" + And I upload "/lib/editor/tiny/tests/behat/fixtures/moodle-logo.mp4" to the file picker for TinyMCE + When I click on "Insert media" "button" + And I select the "video" element in position "1" of the "Description" TinyMCE editor + + @_file_upload + Scenario: Insert and update video in the TinyMCE editor + Given I log in as "admin" + And I open my profile in edit mode + And I click on the "Multimedia" button for the "Description" TinyMCE editor + And I follow "Video" + And I click on "Browse repositories..." "button" in the "#id_description_editor_video .tiny_media_source.tiny_media_media_source" "css_element" + And I upload "/lib/editor/tiny/tests/behat/fixtures/moodle-logo.mp4" to the file picker for TinyMCE + And I click on "Insert media" "button" + And I select the "video" element in position "1" of the "Description" TinyMCE editor + When I click on the "Multimedia" button for the "Description" TinyMCE editor + And I click on "Display options" "link" + And I set the field "Title" to "Test title" + And I click on "Advanced settings" "link" + And I click on "Play automatically" "checkbox" + And I click on "Muted" "checkbox" + And I click on "Loop" "checkbox" + Then "Insert media" "button" should not exist in the "Insert media" "dialogue" + And "Update media" "button" should exist in the "Insert media" "dialogue" + And I click on "Update media" "button" + And I select the "video" element in position "1" of the "Description" TinyMCE editor + And I click on the "Multimedia" button for the "Description" TinyMCE editor + And I click on "Display options" "link" + And the field "Title" matches value "Test title" + And I click on "Advanced settings" "link" + And the field "Play automatically" matches value "1" + And the field "Muted" matches value "1" + And the field "Loop" matches value "1" diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/version.php b/lms-plugins/mediacms-moodle/tiny/mediacms/version.php new file mode 100755 index 00000000..746c7a5c --- /dev/null +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/version.php @@ -0,0 +1,30 @@ +. + +/** + * Tiny media plugin version details. + * + * @package tiny_media + * @copyright 2022 Huong Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2026020103; // Bumped version to ensure upgrade +$plugin->requires = 2024100100; +$plugin->component = 'tiny_mediacms'; +$plugin->dependencies = ['filter_mediacms' => 2026020100]; // Keep dependency on our filter