ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç PKϲ\{xxupload_photos.phpnu[

'.$sub_title.'

' : ''?>
KEMBALI
PKϲ\ PBllfile_categories.phpnu[load->view('backend/grid_index');?> PKϲ\as  files.phpnu[load->view('backend/grid_index');?> PKϲ\$ videos.phpnu[load->view('backend/grid_index');?> PKϲ\˼1,, photos.phpnu[load->view('backend/grid_index');?> PKϲ\ۓ albums.phpnu[ load->view('backend/grid_index');?> PK/<\Z]P styles.cssnu[#tiny_media_form { padding: 1rem; } #tiny_media_form #id_deletefileshdr { display: none; } #tiny_media_form.has-unused-files #id_deletefileshdr { display: block; } #tiny_media_form #id_missingfileshdr { display: none; } #tiny_media_form.has-missing-files #id_missingfileshdr { display: block; } iframe.mm_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_image_form .tiny_image_dropzone_container, .tiny_media_form .tiny_media_dropzone_container { height: 200px; } .tiny_image_form .tiny_image_dropzone_container .dropzone-label, .tiny_media_form .tiny_media_dropzone_container .dropzone-label { font-size: 1.25rem; } .tiny_image_form .tiny_image_loader_container, .tiny_media_form .tiny_media_loader_container { height: 200px; } .tiny_image_form .tiny_image_preview_box { height: 300px; } .tiny_media_form .tiny_media_preview_box { height: 100%; } .tiny_image_form .tiny_image_preview_box, .tiny_media_form .tiny_media_preview_box { display: flex; justify-content: center; align-items: center; overflow: hidden; } .tiny_media_form .tiny_media_preview_box { position: relative; } .tiny_media_form .tiny_media_preview_box img { max-height: 300px; } .tiny_image_form .tiny_image_deleteicon, .tiny_media_form .tiny_media_delete_icon { position: absolute; top: 5px; right: 5px; cursor: pointer; z-index: 1; background: rgba(255, 255, 255, 1); padding: 5px 10px; } .tiny_image_form .tiny_image_deleteicon .fa-trash, .tiny_media_form .tiny_media_delete_icon .fa-trash { color: #1d2125; } @media (max-width: 767px) { .tiny_image_form .tiny_image_properties_col, .tiny_media_form .tiny_media_properties_col { padding: 0; } } .subtitle-caption { text-decoration: underline; } .upload-custom-thumbnail, .delete-custom-thumbnail { width: 100%; } .cs-link { margin-left: -23px; } .dropzone-container .dropzone-icon img.icon { height: .83em; opacity: .4; } .tiny_media_preview_tag { max-height: 500px; } .link_as_audio i, .link_as_video i { font-size: 5em; color: #bdc6cd; } .link_as_audio:hover i, .link_as_video:hover i { color: rgb(97, 97, 97); } #media-filter-preview-container > video { width: 100%; } PK/<\Udb/services.phpnu[. /** * This file contains a controller for receiving Tiny Media service requests. * * @package tiny_media * @copyright 2025 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $functions = [ 'tiny_media_preview' => [ 'classname' => 'tiny_media\external\preview', 'methodname' => 'execute', 'description' => 'Filters the content', 'type' => 'read', 'ajax' => true, ], ]; PK/<\z =ss db/access.phpnu[. /** * Capabilities for the tiny_media plugin. * * @package tiny_media * @copyright 2025 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = [ 'tiny/media:use' => [ 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'user' => CAP_ALLOW, ], ], ]; PK/<\w< lang/en/deprecated.txtnu[helplinktext,tiny_media addsource,tiny_media addsource_help,tiny_media advancedsettings,tiny_media audiosourcelabel,tiny_media browserepositoriesimage,tiny_media createmedia,tiny_media displayoptions,tiny_media entername,tiny_media entersource,tiny_media link,tiny_media tracks_help,tiny_media updatemedia,tiny_media videoheight,tiny_media videosourcelabel,tiny_media videowidth,tiny_media PK/<\\b lang/en/tiny_media.phpnu[. /** * Strings for component 'tiny_media', language 'en'. * * @package tiny_media * @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['addmediafilesdrop'] = 'Drag and drop audio/video file to upload, or click to select'; $string['addmediathumbnaildrop'] = 'Drag and drop image thumbnail file to upload, or click to select'; $string['addmetadatatrack'] = 'Add metadata track'; $string['addsubtitlestrack'] = 'Add subtitle track'; $string['addurl'] = 'Add'; $string['audio'] = 'Audio'; $string['autoplay'] = 'Autoplay'; $string['back'] = 'Back'; $string['browseembedimagerepositories'] = 'Browse repositories'; $string['browserepositories'] = 'Browse repositories...'; $string['captions'] = 'Captions'; $string['captions_help'] = 'Captions may be used to describe everything happening in the track, including non-verbal sounds such as a phone ringing.'; $string['captionssourcelabel'] = 'Caption track URL'; $string['changethumbnail'] = 'Change thumbnail'; $string['chapters'] = 'Chapters'; $string['chapters_help'] = 'Chapter titles may be provided for use in navigating the media resource.'; $string['chapterssourcelabel'] = 'Chapter track URL'; $string['constrain'] = 'Keep proportion'; $string['controls'] = 'Show controls'; $string['customsize_help'] = 'For best viewing experience, the video\'s width and height will adjust together, keeping the original aspect ratio.'; $string['default'] = 'Default'; $string['deleteembeddedthumbnail'] = 'Delete thumbnail'; $string['deleteembeddedthumbnailwarning'] = 'Are you sure you want to remove the embedded thumbnail?'; $string['deleteimage'] = 'Delete image'; $string['deleteimagewarning'] = 'Are you sure you want to remove the image?'; $string['deletemedia'] = 'Delete media'; $string['deletemediathumbnail'] = 'Delete media thumbnail'; $string['deletemediathumbnailwarning'] = 'Are you sure you want to remove the media thumbnail?'; $string['deletemediawarning'] = 'Are you sure you want to remove the media?'; $string['deleteselected'] = 'Delete selected files'; $string['deletethumbnail'] = 'Delete custom thumbnail'; $string['descriptions'] = 'Descriptions'; $string['descriptions_help'] = 'Audio descriptions may be used to provide a narration which explains visual details not apparent from the audio alone.'; $string['descriptionssourcelabel'] = 'Description track URL'; $string['enteralt'] = 'How would you describe this image to someone who can\'t see it?'; $string['entertitle'] = 'Media title'; $string['enterurl'] = 'Add via URL'; $string['enterurlor'] = 'Or add via URL'; $string['filemanager'] = 'File manager'; $string['hasmissingfiles'] = 'Warning! The following files that are referenced in the text area appear to be missing:'; $string['height'] = 'Height'; $string['imagebuttontitle'] = 'Image'; $string['imagedetails'] = 'Image details'; $string['imageproperties'] = 'Image properties'; $string['imageurlrequired'] = 'An image must have a valid URL.'; $string['insertimage'] = 'Insert image'; $string['insertmedia'] = 'Insert media'; $string['insertmediathumbnail'] = 'Insert media thumbnail'; $string['label'] = 'Label'; $string['languagesavailable'] = 'Languages available'; $string['languagesinstalled'] = 'Languages installed'; $string['linkcustomsize_help'] = 'For best viewing experience, the link media\'s width and height will adjust together with 16:9 aspect ratio.'; $string['loading'] = 'Preparing the image'; $string['loadingembedthumbnail'] = 'Preparing the embed thumbnail'; $string['loadingmedia'] = 'Preparing the media'; $string['loop'] = 'Loop'; $string['managefiles'] = 'Manage files'; $string['media:use'] = 'Use TinyMCE insert media'; $string['mediabuttontitle'] = 'Multimedia'; $string['mediadetails'] = 'Media details'; $string['medialinktypeselector'] = 'Select media type'; $string['mediamanagerbuttontitle'] = 'Media manager'; $string['mediamanagerproperties'] = 'Media manager'; $string['medianotavailable'] = 'Media is not available'; $string['medianotavailabledesc'] = 'Error loading the following media url: {$a}'; $string['mediaurlrequired'] = 'An audio/video must have a valid URL.'; $string['metadata'] = 'Metadata'; $string['metadata_help'] = 'Metadata tracks, for use from a script, may be used only if the player supports metadata.'; $string['metadatasourcelabel'] = 'Metadata track URL'; $string['missingfiles'] = 'Missing files'; $string['subtitlesandcaptions'] = 'Subtitles and captions'; $string['mute'] = 'Muted'; $string['next'] = 'Next'; $string['nofilepicker'] = 'Paste a link to an audio/video file in the field below or
click the Browse Repositories button.'; $string['nofilepickerrepository'] = 'Paste an {$a} link in the field below.'; $string['onlymediafiles'] = 'Accepts only media files'; $string['pluginname'] = 'Insert media'; $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 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'] = 'Subtitles'; $string['subtitles_help'] = 'Subtitles may be used to provide a transcription or translation of the dialogue.'; $string['subtitlessourcelabel'] = 'Subtitle track URL'; $string['thumbnail'] = 'Media thumbnail'; $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['uploading'] = 'Uploading'; $string['uploadthumbnail'] = 'Add custom thumbnail'; $string['video'] = 'Video'; $string['width'] = 'Width'; // Deprecated since Moodle 4.5. $string['addsource'] = 'Add alternative source'; $string['addsource_help'] = 'You are recommended to provide an alternative media source, as desktop and mobile browsers support different file formats.'; $string['advancedsettings'] = 'Advanced settings'; $string['audiosourcelabel'] = 'Audio source URL'; $string['browserepositoriesimage'] = 'Browse repositories'; $string['createmedia'] = 'Insert media'; $string['displayoptions'] = 'Display options'; $string['entername'] = 'Name'; $string['entersource'] = 'Source URL'; $string['helplinktext'] = 'Media helper'; $string['link'] = 'Link'; $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['updatemedia'] = 'Update media'; $string['videoheight'] = 'Video height'; $string['videosourcelabel'] = 'Video source URL'; $string['videowidth'] = 'Video width'; PK/<\K4>% % pix/filemanager.svgnu[ PK/<\zҳ pix/icon.svgnu[PK/<\  version.phpnu[. /** * 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 = 2025041400; $plugin->requires = 2025040800; $plugin->component = 'tiny_media'; PK/<\ manage.phpnu[. /** * Manage files in user draft area attached to texteditor. * * @package tiny_media * @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_media'); $url = new moodle_url('/lib/editor/tiny/plugins/media/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_media\form\manage_files_form(null, [ 'context' => $usercontext, 'options' => $options, 'draftitemid' => $itemid, 'files' => $filenames, 'elementid' => $elementid, 'removeorphaneddrafts' => $removeorphaneddrafts, ], 'post', '', [ 'id' => 'tiny_media_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(); PK/<\d"classes/form/manage_files_form.phpnu[. /** * TinyMCE Media plugin form. * * @package tiny_media * @copyright 2022, Stevani Andolo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tiny_media\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_media * @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_media')); $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_media') )); } $mform->addElement('header', 'missingfileshdr', get_string('missingfiles', 'tiny_media')); $mform->addElement('static', '', '', html_writer::tag( 'div', html_writer::tag('div', get_string('hasmissingfiles', 'tiny_media')) . html_writer::tag('div', '', ['class' => 'missing-files'] ), ['class' => 'file-status']) ); $mform->addElement('header', 'deletefileshdr', get_string('unusedfilesheader', 'tiny_media')); $mform->addElement('static', '', '', html_writer::tag('div', get_string('unusedfilesdesc', 'tiny_media'))); foreach ($files as $hash => $file) { $mform->addElement('checkbox', "deletefile[{$hash}]", '', $file, ['data-filename' => $file, 'data-filehash' => $hash]); $mform->setType("deletefile[{$hash}]", PARAM_INT); } $mform->addElement('submit', 'delete', get_string('deleteselected', 'tiny_media')); $PAGE->requires->js_call_amd('tiny_media/usedfiles', 'init', [ 'usercontext' => $usercontext->id, 'itemid' => $itemid, 'elementid' => $elementid, ]); } } PK/<\!  classes/external/preview.phpnu[. namespace tiny_media\external; use context; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; /** * TinyMCE Media external API for filtering the content. * * @package tiny_media * @copyright 2025 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class preview extends external_api { /** * Describes the parameters for filtering the content. * * @return external_function_parameters * @since Moodle 5.0 */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters([ 'contextid' => new external_value(PARAM_INT, 'The context ID', VALUE_REQUIRED), 'content' => new external_value(PARAM_RAW, 'The media content', VALUE_REQUIRED), ]); } /** * External function to filter the content. * * @param int $contextid Context ID. * @param string $content Equation content. * @return array * @since Moodle 5.0 */ public static function execute( int $contextid, string $content, ): array { [ 'contextid' => $contextid, 'content' => $content, ] = self::validate_parameters(self::execute_parameters(), [ 'contextid' => $contextid, 'content' => $content, ]); $context = context::instance_by_id($contextid); self::validate_context($context); [$result, $format] = \core_external\util::format_text($content, FORMAT_HTML, $context, 'tiny_media'); return [ 'content' => $result, ]; } /** * Describes the data returned from the external function. * * @return external_single_structure * @since Moodle 5.0 */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'content' => new external_value(PARAM_RAW, 'Filtered content'), ]); } } PK/<\FXXclasses/privacy/provider.phpnu[. namespace tiny_media\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'; } } PK/<\Qclasses/plugininfo.phpnu[. namespace tiny_media; use context; 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; /** * 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 { #[\Override] 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. // - Don't have the correct capability. $canhavefiles = !empty($options['maxfiles']); $canhaveexternalfiles = !empty($options['return_types']) && ($options['return_types'] & FILE_EXTERNAL); return isloggedin() && !isguestuser() && ($canhavefiles || $canhaveexternalfiles) && has_capability('tiny/media:use', $context); } #[\Override] public static function is_enabled_for_external(context $context, array $options): bool { // Assume files are allowed. $options['maxfiles'] = 1; return self::is_enabled($context, $options, []); } public static function get_available_buttons(): array { return [ 'tiny_media/tiny_media_image', ]; } public static function get_available_menuitems(): array { return [ 'tiny_media/tiny_media_image', ]; } 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, ] ]; return array_merge([ 'permissions' => $permissions, ], self::get_file_manager_configuration($context, $options, $fpoptions)); } 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, ], ]; } } PK/<\ڌ)templates/missingfiles.mustachenu[{{! 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_media/missingfiles Insert image template. Example context (json): { "missingFiles": [ "Example.png", "Another.mov" ] } }}
    {{#missingFiles}}
  1. {{.}}
  2. {{/missingFiles}}
PK/<\>άyggtemplates/mm_iframe.mustachenu[{{! 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_media/mm_iframe Insert image template. Example context (json): { } }} PK/<\b=ootemplates/image.mustachenu[{{! 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_media/image Image template. Example context (json): { } }} {{alt}} PK/<\O  3templates/insert_image_modal_insert_footer.mustachenu[{{! 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_media/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}}
PK/<\_kk-templates/insert_image_modal_details.mustachenu[{{! 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_media/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" }, "maxlengthalt": 750 } }}
0 / {{maxlengthalt}}
X
{{#sizecustomhelpicon}}{{> core/help_icon }}{{/sizecustomhelpicon}}
PK/<\t,templates/insert_image_modal_insert.mustachenu[{{! 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_media/insert_image_modal_insert Insert image body template. Example context (json): { "showdropzone": true, "showfilepicker": true } }}
{{#showdropzone}}
{{/showdropzone}} {{^showdropzone}}

{{#showfilepicker}} {{#str}} repositoryuploadnotpermitted, tiny_media {{/str}}

{{/showfilepicker}} {{^showfilepicker}} {{#str}} repositorynotpermitted, tiny_media {{/str}} {{/showfilepicker}}
{{/showdropzone}}
PK/<\;FԱ``*templates/embed/embed_media_audio.mustachenu[{{! 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_media/embed_media_audio Embed media audio template. Example context (json): { } }}   {{#sources}}{{/sources}} {{#tracks}} {{/tracks}} {{#description}}{{.}}{{/description}}   PK/<\V46templates/embed/insert_media_subtitle_caption.mustachenu[{{! 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_media/embed/insert_media_subtitle_caption Insert media subtitles, captions, chapters, descriptions and metadata template. Example context (json): { "elementid": "id", "subtitleshelpicon": "subtitle help icon", "captionshelpicon": "caption help icon", "descriptionshelpicon": "description help icon", "chaptershelpicon": "chapter help icon", "metadatahelpicon": "metadata help icon", "helpStrings": { "metadata": "metadata" } } }} PK/<\vKQ)templates/embed/embed_media_link.mustachenu[{{! 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_media/embed/embed_media_link Embed media link template. Example context (json): { "name": "Media title", "url": "/" } }} {{#name}}{{.}}{{/name}} {{^name}}{{url}}{{/name}} PK/<\UM/templates/embed/body/insert_media_body.mustachenu[{{! 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_media/embed/body/insert_media_body Insert media body template. Example context (json): { "showDropzone": true, "showFilePicker": true } }}
{{#showDropzone}}
{{/showDropzone}} {{^showDropzone}}

{{#showFilePicker}} {{#str}} nofilepicker, tiny_media {{/str}} {{/showFilePicker}} {{^showFilePicker}} {{#str}} nofilepickerrepository, tiny_media, {{fileType}} {{/str}} {{/showFilePicker}}

{{/showDropzone}}
PK/<\pZ((0templates/embed/body/media_details_body.mustachenu[{{! 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_media/embed/body/media_details_body Media preview body template. Example context (json): { "isLink": true, "isVideo": true, "isAudioTag": true, "isVideoTag": true, "elementid": "id", "media" : { "poster": "link/to/poster", "title": "media name/title", "isUpdating": true, "controls": true, "autoplay": false, "muted": false, "loop": false, "width": "1200", "height": "800" } } }}
{{#isVideo}}
{{#media.poster}}
{{/media.poster}}
{{/isVideo}}
{{#showControl}}
{{#isVideo}}

{{#isLink}} {{#linkcustomsizehelpicon}} {{> core/help_icon }} {{/linkcustomsizehelpicon}} {{/isLink}} {{^isLink}} {{#customsizehelpicon}} {{> core/help_icon }} {{/customsizehelpicon}} {{/isLink}}
X
{{/isVideo}} {{/showControl}}
{{> tiny_media/embed/insert_media_subtitle_caption }}
PK/<\ 2templates/embed/body/media_thumbnail_body.mustachenu[{{! 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_media/embed/body/media_thumbnail_body Insert thumbnail preview body template. Example context (json): { } }}
media thumbnail image
PK/<\")yy*templates/embed/embed_media_modal.mustachenu[{{! 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_media/embed_media_modal Embed media modal template. Example context (json): { } }} {{< core/modal }} {{$body}}
{{> core/loading }}
{{#str}} loadingmedia, tiny_media {{/str}}
{{/body}} {{$footer}} {{/footer}} {{/ core/modal }} PK/<\I.JJ*templates/embed/embed_media_video.mustachenu[{{! 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_media/embed_media_video Embed media video template. Example context (json): { } }}   {{#sources}} {{/sources}} {{#tracks}} {{/tracks}} {{#description}}{{.}}{{/description}}   PK/<\yܧ~.templates/embed/metadata/descriptions.mustachenu[{{! 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_media/embed/metadata/descriptions Insert media descriptions template. Example context (json): { "media": { "tracks": { "descriptions": "descriptions" } }, "showFilePickerTrack": true, "langsInstalled": "installed langs", "src": "http://moodle.com/link/to/source", "srclang": "selected lang", "default": true, "langsAvailable": "avalialbe langs", "defaultTrack": true, "label": "selected track", "code": "lang code", "lang": "lang name" } }} {{#media.tracks.descriptions}}
{{#showFilePickerTrack}} {{/showFilePickerTrack}}
{{/media.tracks.descriptions}} {{^media.tracks.descriptions}}
{{#showFilePickerTrack}} {{/showFilePickerTrack}}
{{/media.tracks.descriptions}} PK/<\__*templates/embed/metadata/metadata.mustachenu[{{! 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_media/embed/metadata/metadata Insert media metadata template. Example context (json): { "media": { "tracks": { "metadata": "metadata" } }, "showFilePickerTrack": true, "langsInstalled": "installed langs", "src": "http://moodle.com/link/to/source", "srclang": "selected lang", "default": true, "langsAvailable": "avalialbe langs", "defaultTrack": true, "label": "selected track", "code": "lang code", "lang": "lang name" } }} {{#media.tracks.metadata}}
{{#showFilePickerTrack}} {{/showFilePickerTrack}}
{{/media.tracks.metadata}} {{^media.tracks.metadata}}
{{#showFilePickerTrack}} {{/showFilePickerTrack}}
{{/media.tracks.metadata}} PK/<\E2zz+templates/embed/metadata/subtitles.mustachenu[{{! 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_media/embed/metadata/subtitles Insert media subtitles template. Example context (json): { "media": { "tracks": { "subtitles": "subtitles" } }, "showFilePickerTrack": true, "langsInstalled": "installed langs", "src": "http://moodle.com/link/to/source", "srclang": "selected lang", "default": true, "langsAvailable": "avalialbe langs", "defaultTrack": true, "label": "selected track", "code": "lang code", "lang": "lang name" } }} {{#media.tracks.subtitles}}
{{#showFilePickerTrack}} {{/showFilePickerTrack}}
{{/media.tracks.subtitles}} {{^media.tracks.subtitles}}
{{#showFilePickerTrack}} {{/showFilePickerTrack}}
{{/media.tracks.subtitles}} PK/<\#SS*templates/embed/metadata/chapters.mustachenu[{{! 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_media/embed/metadata/chapters Insert media chapters template. Example context (json): { "media": { "tracks": { "chapters": "chapters" } }, "showFilePickerTrack": true, "langsInstalled": "installed langs", "src": "http://moodle.com/link/to/source", "srclang": "selected lang", "default": true, "langsAvailable": "avalialbe langs", "defaultTrack": true, "label": "selected track", "code": "lang code", "lang": "lang name" } }} {{#media.tracks.chapters}}
{{#showFilePickerTrack}} {{/showFilePickerTrack}}
{{/media.tracks.chapters}} {{^media.tracks.chapters}}
{{#showFilePickerTrack}} {{/showFilePickerTrack}}
{{/media.tracks.chapters}} PK/<\\QQ*templates/embed/metadata/captions.mustachenu[{{! 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_media/embed/metadata/captions Insert media captions template. Example context (json): { "media": { "tracks": { "captions": "captions" } }, "showFilePickerTrack": true, "langsInstalled": "installed langs", "src": "http://moodle.com/link/to/source", "srclang": "selected lang", "default": true, "langsAvailable": "avalialbe langs", "defaultTrack": true, "label": "selected track", "code": "lang code", "lang": "lang name" } }} {{#media.tracks.captions}}
{{#showFilePickerTrack}} {{/showFilePickerTrack}}
{{/media.tracks.captions}} {{^media.tracks.captions}}
{{#showFilePickerTrack}} {{/showFilePickerTrack}}
{{/media.tracks.captions}} PK/<\ "} } 3templates/embed/footer/insert_media_footer.mustachenu[{{! 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_media/embed/footer/insert_media_footer Insert media footer template. Example context (json): { "elementid": "id", "isInsertThumbnail": false, "showDropzone": true, "showFilePicker": true, "showImageFilePicker": true } }}
{{#showDropzone}} {{/showDropzone}} {{^showDropzone}} {{/showDropzone}}
{{#showFilePicker}} {{/showFilePicker}} {{#showImageFilePicker}} {{/showImageFilePicker}}
PK/<\4CC6templates/embed/footer/media_thumbnail_footer.mustachenu[{{! 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_media/embed/footer/media_thumbnail_footer Media thumbnail preview footer template. Example context (json): { } }} PK/<\62@4templates/embed/footer/media_details_footer.mustachenu[{{! 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_media/embed/footer/media_details_footer Media preview footer template. Example context (json): { } }} PK/<\]  4templates/insert_image_modal_details_footer.mustachenu[{{! 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_media/insert_image_modal_details_footer Insert image details footer template. Example context (json): { "elementid": "exampleId" } }}
PK/<\-ذ%templates/insert_image_modal.mustachenu[{{! 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_media/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_media {{/str}}
{{/body}} {{$footer}} {{/footer}} {{/ core/modal }} PK/<\hL amd/src/configuration.jsnu[// 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_media/configuration * @copyright 2022 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import { imageButtonName, videoButtonName, mediaManagerButtonName, } from './common'; import uploadFile from 'editor_tiny/uploader'; import { addContextmenuItem, } from 'editor_tiny/utils'; const configureMenu = (menu) => { // Replace the standard Media plugin with the Moodle embed. if (menu.insert.items.match(/\bmedia\b/)) { menu.insert.items = menu.insert.items.replace(/\bmedia\b/, videoButtonName); } else { menu.insert.items = `${videoButtonName} ${menu.insert.items}`; } // Replace the standard image plugin with the Moodle image. if (menu.insert.items.match(/\bimage\b/)) { menu.insert.items = menu.insert.items.replace(/\bimage\b/, imageButtonName); } else { menu.insert.items = `${imageButtonName} ${menu.insert.items}`; } // Add the Media Manager to the end of the Tools menu. menu.tools.items += ` ${mediaManagerButtonName}`; 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 image, and embed, buttons at the start of it. section.items.unshift(imageButtonName, videoButtonName); } return section; }); }; export const configure = (instanceConfig) => { // Update the instance configuration to add the Media menu option to the menus and toolbars and upload_handler. return { contextmenu: addContextmenuItem(instanceConfig.contextmenu, imageButtonName, videoButtonName), menu: configureMenu(instanceConfig.menu), toolbar: configureToolbar(instanceConfig.toolbar), // eslint-disable-next-line camelcase images_upload_handler: (blobInfo, progress) => uploadFile( window.tinymce.activeEditor, 'image', blobInfo.blob(), blobInfo.filename(), progress ), // eslint-disable-next-line camelcase images_reuse_filename: true, }; }; PK/<\{USMMamd/src/imagedetails.jsnu[// 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_media/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_media/imageinsert'; import {MediaBase} from './mediabase'; import { body, footer, hideElements, showElements, isPercentageValue, } from './helpers'; export class ImageDetails extends MediaBase { DEFAULTS = { WIDTH: 160, HEIGHT: 160, }; selectorType = Selectors.IMAGE.type; mediaDimensions = null; constructor( root, editor, currentModal, canShowFilePicker, canShowDropZone, currentUrl, image, ) { super(); this.root = root; this.editor = editor; this.currentModal = currentModal; this.canShowFilePicker = canShowFilePicker; this.canShowDropZone = canShowDropZone; this.currentUrl = currentUrl; this.image = image; this.toggleMaxlengthFeedbackSuffix = false; } init = function() { this.currentModal.setTitle(getString('imagedetails', 'tiny_media')); 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, bodyTemplate: Selectors.IMAGE.template.body.insertImageBody, footerTemplate: Selectors.IMAGE.template.footer.insertImageFooter, selector: Selectors.IMAGE.type, }; Promise.all([body(templateContext, this.root), footer(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.mediaDimensions = { width: image.width || this.DEFAULTS.WIDTH, height: image.height || this.DEFAULTS.HEIGHT, }; const getCurrentWidth = (element) => { if (element.value === '') { element.value = this.mediaDimensions.width; } return element.value; }; const getCurrentHeight = (element) => { if (element.value === '') { element.value = this.mediaDimensions.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.mediaDimensions.width === currentWidth && this.mediaDimensions.height === currentHeight ) { this.currentWidth = this.mediaDimensions.width; this.currentHeight = this.mediaDimensions.height; this.sizeChecked('original'); } else { this.currentWidth = currentWidth; this.currentHeight = currentHeight; this.sizeChecked('custom'); } }; setSelectedSize(Number(currentWidth), Number(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 changes in the image presentation checkbox and enables/disables the image alt text input accordingly. */ async 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. await 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.altWarning, 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_media: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_media/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_media'), getString('deleteimagewarning', 'tiny_media'), ).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', async(e) => { const presentationEle = e.target.closest(Selectors.IMAGE.elements.presentation); if (presentationEle) { await 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', async(e) => { if (e.target.nodeType === Node.ELEMENT_NODE) { const presentationEle = e.target.closest(Selectors.IMAGE.elements.presentation); if (presentationEle) { await this.presentationChanged(); } } }, true); // Character count. this.root.addEventListener('keyup', async(e) => { const altEle = e.target.closest(Selectors.IMAGE.elements.alt); if (altEle) { await 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); } }); } async handleKeyupCharacterCount() { const altField = this.root.querySelector(Selectors.IMAGE.elements.alt); const alt = altField.value; const current = this.root.querySelector('#currentcount'); current.innerHTML = alt.length; const maxLength = altField.getAttribute('maxlength'); const maxLengthFeedback = document.getElementById('maxlength_feedback'); if (alt.length >= maxLength) { maxLengthFeedback.textContent = await getString('maxlengthreached', 'core', maxLength); // Clever (or hacky?;p) way to ensure that the feedback message is announced to screen readers. const suffix = this.toggleMaxlengthFeedbackSuffix ? '' : '.'; maxLengthFeedback.textContent += suffix; this.toggleMaxlengthFeedbackSuffix = !this.toggleMaxlengthFeedbackSuffix; // Clear the feedback message after 4 seconds. This is similar to the default timeout of toast messages // before disappearing from view. It is important to clear the message to prevent screen reader users from navigating // into this region and avoiding confusion. setTimeout(() => { maxLengthFeedback.textContent = ''; }, 4000); } } /** * 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, }; }; } PK/<\eDbamd/src/mediabase.jsnu[// 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 class helpers for image and embed. * * @module tiny_media/mediabase * @copyright 2024 Stevani Andolo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import { isPercentageValue, hideElements, showElements, } from './helpers'; import Selectors from './selectors'; export class MediaBase { /** * Handles the selection of media size options and updates the form inputs accordingly. * * @param {string} option - The selected media size option ("original" or "custom"). */ sizeChecked = async(option) => { const widthInput = this.root.querySelector(Selectors[this.selectorType].elements.width); const heightInput = this.root.querySelector(Selectors[this.selectorType].elements.height); if (option === "original") { this.sizeOriginalChecked(); widthInput.value = this.mediaDimensions.width; heightInput.value = this.mediaDimensions.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 and selectorType = IMAGE, // then check the Keep proportion checkbox. if ( this.selectorType === Selectors.IMAGE.type && this.currentWidth === this.mediaDimensions.width && this.currentHeight === this.mediaDimensions.height ) { const constrainField = this.root.querySelector(Selectors[this.selectorType].elements.constrain); constrainField.checked = true; } } this.autoAdjustSize(); }; /** * Handles the selection of the "Original Size" option and updates the form elements accordingly. */ sizeOriginalChecked() { this.root.querySelector(Selectors[this.selectorType].elements.sizeOriginal).checked = true; this.root.querySelector(Selectors[this.selectorType].elements.sizeCustom).checked = false; hideElements(Selectors[this.selectorType].elements.properties, this.root); } /** * Handles the selection of the "Custom Size" option and updates the form elements accordingly. */ sizeCustomChecked() { this.root.querySelector(Selectors[this.selectorType].elements.sizeOriginal).checked = false; this.root.querySelector(Selectors[this.selectorType].elements.sizeCustom).checked = true; showElements(Selectors[this.selectorType].elements.properties, this.root); } /** * Auto adjust the media width/height. * It is put here so image.js and/or friends can extend this class and call this for media proportion. * * @param {boolean} forceHeight Whether set by height or not */ autoAdjustSize = (forceHeight = false) => { // If we do not know the media size, do not do anything. if (!this.mediaDimensions) { return; } const widthField = this.root.querySelector(Selectors[this.selectorType].elements.width); const heightField = this.root.querySelector(Selectors[this.selectorType].elements.height); const normalizeFieldData = (fieldData) => { fieldData.isPercentageValue = isPercentageValue(fieldData.field.value); if (fieldData.isPercentageValue) { fieldData.percentValue = parseInt(fieldData.field.value, 10); fieldData.pixelSize = this.mediaDimensions[fieldData.type] / 100 * fieldData.percentValue; } else { fieldData.pixelSize = parseInt(fieldData.field.value, 10); fieldData.percentValue = fieldData.pixelSize / this.mediaDimensions[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.mediaDimensions[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[this.selectorType].elements.constrain); // Only image. if ((constrainField && constrainField.checked) || this.mediaType === 'video') { const keyField = getKeyField(); const relativeField = getRelativeField(); // We are keeping the media 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.mediaDimensions[keyField.type] * this.mediaDimensions[relativeField.type] ); relativeField.field.value = relativeField.pixelSize; } } if (this.selectorType === Selectors.IMAGE.type) { // Store the custom width and height to reuse. this.currentWidth = Number(widthField.value) !== this.mediaDimensions.width ? widthField.value : this.currentWidth; this.currentHeight = Number(heightField.value) !== this.mediaDimensions.height ? heightField.value : this.currentHeight; } }; } PK/<\3+QQamd/src/embed.jsnu[// 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_media/embed * @copyright 2022 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import EmbedModal from './embedmodal'; import {getEmbedPermissions} from './options'; import {getFilePicker, getContextId} from 'editor_tiny/options'; import {EmbedHandler} from './embed/embedhandler'; import {insertMediaTemplateContext, getSelectedMediaElement} from './embed/embedhelpers'; import {EmbedInsert} from './embed/embedinsert'; import {startMediaLoading} from './helpers'; import Selectors from "./selectors"; export default class MediaEmbed { editor = null; canShowFilePicker = false; canShowFilePickerTrack = false; canShowDropZone = false; constructor(editor) { const permissions = getEmbedPermissions(editor); const options = getFilePicker(editor, 'media'); // Indicates whether the file picker can be shown. this.canShowFilePicker = permissions.filepicker && (typeof options !== 'undefined') && Object.keys(options.repositories).length > 0; this.canShowFilePickerTrack = permissions.filepicker && (typeof getFilePicker(editor, 'subtitle') !== 'undefined'); this.canShowDropZone = Object.values(options.repositories).some(repository => repository.type === 'upload'); this.editor = editor; this.acceptedMediaTypes = options.accepted_types; this.contextId = getContextId(editor); // Image options. const imageOptions = getFilePicker(editor, 'image'); this.acceptedImageTypes = imageOptions.accepted_types; this.canShowImageFilePicker = permissions.filepicker && (typeof imageOptions !== 'undefined') && Object.keys(imageOptions.repositories).length > 0; } /** * Displays media modal accordingly. */ displayDialogue = async() => { const [mediaType, selectedMedia] = getSelectedMediaElement(this.editor); this.mediaType = mediaType; this.selectedMedia = selectedMedia; if (this.selectedMedia) { // Preview the selected media. this.isUpdating = true; this.loadSelectedMedia(); } else { // Create media modal. await this.createMediaModal(); // Load insert media modal. await this.loadInsertMediaModal(); } }; /** * Load insert media modal. */ loadInsertMediaModal = async() => { const embedHandler = new EmbedHandler(this); embedHandler.loadTemplatePromise(insertMediaTemplateContext(this)); await embedHandler.registerEventListeners(); }; /** * Create media modal. */ createMediaModal = async() => { this.currentModal = await EmbedModal.create({ large: true, templateContext: {elementid: this.editor.getElement().id}, }); this.modalRoot = this.currentModal.getRoot(); this.root = this.modalRoot[0]; }; /** * Load media preview based on the selected media. */ loadSelectedMedia = async() => { let mediaSource = null; if (['video', 'audio'].includes(this.mediaType)) { mediaSource = this.selectedMedia.querySelector('source').src; // If the selected media has more than one sources, it has main source and alternative sources. const sources = this.selectedMedia.querySelectorAll('source'); if (sources.length > 1) { let alternativeSources = []; Object.keys(sources).forEach(function(source) { alternativeSources.push(sources[source].src); }); this.alternativeSources = alternativeSources; // Used to later check if the embedded media has alternative sources. } } else if (this.selectedMedia.classList.contains(Selectors.EMBED.externalMediaProvider)) { mediaSource = this.selectedMedia.href; this.mediaType = 'link'; } // Load media preview. if (this.mediaType) { // Create media modal. await this.createMediaModal(); // Start the spinner. startMediaLoading(this.root, Selectors.EMBED.type); const embedInsert = new EmbedInsert(this); embedInsert.init(); embedInsert.loadMediaPreview(mediaSource); await (new EmbedHandler(this)).registerEventListeners(); } else { // Create media modal. await this.createMediaModal(); // Load insert media modal. this.loadInsertMediaModal(); } }; } PK/<\.4LLamd/src/imagehelpers.jsnu[// 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_media/imagehelpers * @copyright 2024 Meirza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import * as Helpers from 'tiny_media/helpers'; import {Selectors} from './selectors'; /** * Maximum length allowed for the alt attribute. */ export const MAX_LENGTH_ALT = 750; /** * 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} * @deprecated Since Moodle 5.0 See {@link module:core_editor/tiny/plugins/media/helpers.body} */ export const bodyImageInsert = async(templateContext, root) => { window.console.warn('This function is deprecated. Please use core_editor/tiny/plugins/media/helpers.body instead.'); templateContext.bodyTemplate = 'tiny_media/insert_image_modal_insert'; templateContext.selector = Selectors.IMAGE.type; return Helpers.body(templateContext, root); }; /** * 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} * @deprecated Since Moodle 5.0 See {@link module:core_editor/tiny/plugins/media/helpers.footer} */ export const footerImageInsert = async(templateContext, root) => { window.console.warn(`This function is deprecated. Please use core_editor/tiny/plugins/media/helpers.footer instead.`); templateContext.footerTemplate = 'tiny_media/insert_image_modal_insert_footer'; templateContext.selector = Selectors.IMAGE.type; return Helpers.footer(templateContext, root); }; /** * 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} * @deprecated Since Moodle 5.0 See {@link module:core_editor/tiny/plugins/media/helpers.body} */ export const bodyImageDetails = async(templateContext, root) => { window.console.warn(`This function is deprecated. Please use core_editor/tiny/plugins/media/helpers.body instead.`); templateContext.bodyTemplate = 'tiny_media/insert_image_modal_details'; templateContext.selector = Selectors.IMAGE.type; return Helpers.body(templateContext, root); }; /** * 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} * @deprecated Since Moodle 5.0 See {@link module:core_editor/tiny/plugins/media/helpers.footer} */ export const footerImageDetails = async(templateContext, root) => { window.console.warn(`This function is deprecated. Please use core_editor/tiny/plugins/media/helpers.footer instead.`); templateContext.footerTemplate = 'tiny_media/insert_image_modal_details_footer'; templateContext.selector = Selectors.IMAGE.type; return Helpers.footer(templateContext, root); }; /** * 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. * @deprecated Since Moodle 5.0 See {@link module:core_editor/tiny/plugins/media/helpers.showElements} */ export const showElements = (elements, root) => { window.console.warn(`This function is deprecated. Please use core_editor/tiny/plugins/media/helpers.showElements instead.`); Helpers.showElements(elements, root); }; /** * 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. * @deprecated Since Moodle 5.0 See {@link module:core_editor/tiny/plugins/media/helpers.hideElements} */ export const hideElements = (elements, root) => { window.console.warn(`This function is deprecated. Please use core_editor/tiny/plugins/media/helpers.hideElements instead.`); Helpers.hideElements(elements, root); }; /** * 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. * @deprecated Since Moodle 5.0 See {@link module:core_editor/tiny/plugins/media/helpers.isPercentageValue} */ export const isPercentageValue = (value) => { window.console.warn(`This function is deprecated. Please use core_editor/tiny/plugins/media/helpers.isPercentageValue instead.`); return Helpers.isPercentageValue(value); }; PK/<\# # amd/src/options.jsnu[// 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_media/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'); /** * 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', }); }; /** * 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); PK/<\~amd/src/embedmodal.jsnu[// 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_media/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/embed_media_modal`; registerEventListeners() { // Call the parent registration. super.registerEventListeners(); // Register to close on save/cancel. this.registerCloseOnSave(); this.registerCloseOnCancel(); } configure(modalConfig) { modalConfig.removeOnClose = true; modalConfig.show = true; super.configure(modalConfig); } } PK/<\5QQamd/src/common.jsnu[// 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_media/common * @copyright 2022 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ export default { pluginName: 'tiny_media/plugin', component: 'tiny_media', imageButtonName: 'tiny_media_image', videoButtonName: 'tiny_media_video', mediaManagerButtonName: 'tiny_mediamanager', }; PK/<\j ͐88amd/src/commands.jsnu[// 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_media/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, imageButtonName, videoButtonName, mediaManagerButtonName } from './common'; import MediaImage from './image'; import MediaEmbed from './embed'; import MediaManager from './manager'; import {getButtonImage} from 'editor_tiny/utils'; import Selectors from './selectors'; const isImage = (node) => node.nodeName.toLowerCase() === 'img'; const isVideo = (node) => node.nodeName.toLowerCase() === 'video' || node.nodeName.toLowerCase() === 'audio'; const registerImageCommand = (editor, imageButtonText) => { const imageIcon = 'image'; const handleImageAction = () => { const mediaImage = new MediaImage(editor); mediaImage.displayDialogue(); }; // Register the Menu Button as a toggle. // This means that when highlighted over an existing Media Image element it will show as toggled on. editor.ui.registry.addToggleButton(imageButtonName, { icon: imageIcon, tooltip: imageButtonText, onAction: handleImageAction, onSetup: api => { return editor.selection.selectorChangedWithUnbind( 'img:not([data-mce-object]):not([data-mce-placeholder]),figure.image', api.setActive ).unbind; } }); editor.ui.registry.addMenuItem(imageButtonName, { icon: imageIcon, text: imageButtonText, onAction: handleImageAction, }); editor.ui.registry.addContextToolbar(imageButtonName, { predicate: isImage, items: imageButtonName, position: 'node', scope: 'node' }); editor.ui.registry.addContextMenu(imageButtonName, { update: isImage, }); // Let's check for image file at dragged and dropped and add img-fluid class if it does't have it yet. editor.on('SetContent', function() { const imgs = editor.getBody().querySelectorAll(`img:not(.${Selectors.IMAGE.styles.responsive})`); imgs.forEach(img => img.classList.add(`${Selectors.IMAGE.styles.responsive}`)); }); }; const registerEmbedCommand = (editor, videoButtonText) => { const videoIcon = 'embed'; const handleEmbedAction = () => { const mediaImage = new MediaEmbed(editor); mediaImage.displayDialogue(); }; // Register the Menu Button as a toggle. // This means that when highlighted over an existing Media Video element it will show as toggled on. editor.ui.registry.addToggleButton(videoButtonName, { icon: videoIcon, tooltip: videoButtonText, onAction: handleEmbedAction, onSetup: api => { return editor.selection.selectorChangedWithUnbind( 'video:not([data-mce-object]):not([data-mce-placeholder]),' + 'audio:not([data-mce-object]):not([data-mce-placeholder]),' + 'a:not([data-mce-object]):not([data-mce-placeholder])[class=external-media-provider]', api.setActive ).unbind; } }); editor.ui.registry.addMenuItem(videoButtonName, { icon: videoIcon, text: videoButtonText, onAction: handleEmbedAction, }); editor.ui.registry.addContextMenu(videoButtonName, { update: isVideo, }); editor.ui.registry.addContextToolbar(videoButtonName, { predicate: isVideo, items: videoButtonName, position: 'node', scope: 'node' }); }; const registerManagerCommand = (editor, mediaManagerButtonText, mediaManagerButtonImage) => { const mediaManagerIcon = 'filemanager'; const handleMediaManager = () => { const mediaManager = new MediaManager(editor); mediaManager.displayDialogue(); }; // Register the Menu Button as a toggle. editor.ui.registry.addIcon(mediaManagerIcon, mediaManagerButtonImage.html); editor.ui.registry.addButton(mediaManagerButtonName, { icon: mediaManagerIcon, tooltip: mediaManagerButtonText, onAction: () => { handleMediaManager(); } }); editor.ui.registry.addMenuItem(mediaManagerButtonName, { icon: mediaManagerIcon, text: mediaManagerButtonText, onAction: () => { handleMediaManager(); } }); }; export const getSetup = async() => { const [ imageButtonText, mediaButtonText, mediaManagerButtonText ] = await getStrings(['imagebuttontitle', 'mediabuttontitle', 'mediamanagerbuttontitle'].map((key) => ({key, component}))); const [ mediaManagerButtonImage, ] = await Promise.all([ getButtonImage('filemanager', 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) => { registerImageCommand(editor, imageButtonText); registerEmbedCommand(editor, mediaButtonText); registerManagerCommand(editor, mediaManagerButtonText, mediaManagerButtonImage); }; }; PK/<\[:''amd/src/imageinsert.jsnu[// 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_media/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_media/imagedetails'; import { body, footer, hideElements, showElements, isValidUrl, } from './helpers'; import {MAX_LENGTH_ALT} from './imagehelpers'; prefetchStrings('tiny_media', [ '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 !== "" && isValidUrl(url)); } /** * 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}; templateContext.bodyTemplate = Selectors.IMAGE.template.body.insertImageDetailsBody; templateContext.footerTemplate = Selectors.IMAGE.template.footer.insertImageDetailsFooter; templateContext.selector = Selectors.IMAGE.type; templateContext.maxlengthalt = MAX_LENGTH_ALT; Promise.all([body(templateContext, this.root), footer(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); }); } } } PK/<\KKamd/src/plugin.jsnu[// 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_media/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'; // 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); return pluginMetadata; }); // Resolve the Media Plugin and include configuration. resolve([`${component}/plugin`, Configuration]); }); PK/<\b$$amd/src/image.jsnu[// 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_media/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_media/imageinsert'; import {ImageDetails} from 'tiny_media/imagedetails'; import {prefetchStrings} from 'core/prefetch'; import {getString} from 'core/str'; import { body, footer, hideElements, showElements, isPercentageValue, } from './helpers'; import {MAX_LENGTH_ALT} from './imagehelpers'; prefetchStrings('tiny_media', [ '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, bodyTemplate: Selectors.IMAGE.template.body.insertImageBody, footerTemplate: Selectors.IMAGE.template.footer.insertImageFooter, selector: Selectors.IMAGE.type, }; Promise.all([body(templateContext, this.root), footer(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_media'); 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_media')}; templateContext.bodyTemplate = Selectors.IMAGE.template.body.insertImageDetailsBody; templateContext.footerTemplate = Selectors.IMAGE.template.footer.insertImageDetailsFooter; templateContext.selector = Selectors.IMAGE.type; templateContext.maxlengthalt = MAX_LENGTH_ALT; Promise.all([body(templateContext, this.root), footer(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); } } PK/<\ 0y>y>amd/src/embed/embedpreview.jsnu[// 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 preview and details class. * * This handles the embed file/url preview before embedding them into tiny editor. * * @module tiny_media/embed/embedpreview * @copyright 2024 Stevani Andolo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Selectors from '../selectors'; import {component} from '../common'; import {getString} from 'core/str'; import { sourceTypeChecked, getFileName, setPropertiesFromData, showElements, stopMediaLoading, hideElements, } from '../helpers'; import {EmbedHandler} from './embedhandler'; import {MediaBase} from '../mediabase'; import Notification from 'core/notification'; import EmbedModal from '../embedmodal'; import { getEmbeddedMediaDetails, insertMediaThumbnailTemplateContext, fetchPreview, } from './embedhelpers'; import {notifyFilterContentUpdated} from 'core_filters/events'; export class EmbedPreview extends MediaBase { // Selector type for "EMBED". selectorType = Selectors.EMBED.type; // Fixed aspect ratio used for external media providers. linkMediaAspectRatio = 1.78; constructor(data) { super(); setPropertiesFromData(this, data); // Creates dynamic properties based on "data" param. } /** * Init the media details preview. */ init = async() => { this.currentModal.setTitle(getString('mediadetails', component)); sourceTypeChecked({ fetchedTitle: this.fetchedMediaLinkTitle ?? null, source: this.originalUrl, root: this.root, urlSelector: Selectors.EMBED.elements.fromUrl, fileNameSelector: Selectors.EMBED.elements.fileNameLabel, }); this.setMediaSourceAndPoster(); this.registerMediaDetailsEventListeners(this.currentModal); }; /** * Sets media source and thumbnail for the video. */ setMediaSourceAndPoster = async() => { const box = this.root.querySelector(Selectors.EMBED.elements.previewBox); const previewArea = document.querySelector(Selectors.EMBED.elements.mediaPreviewContainer); previewArea.setAttribute('data-original-url', this.originalUrl); // Previewing existing media could be a link one. // Or, new media added using url input and mediaType is neither video or audio. if (this.mediaType === 'link' || (this.newMediaLink && !['video', 'audio'].includes(this.mediaType))) { previewArea.setAttribute('data-media-type', 'link'); previewArea.innerHTML = await fetchPreview(this.originalUrl, this.contextId); notifyFilterContentUpdated(previewArea); } else if (this.mediaType === 'video') { const video = document.createElement('video'); video.src = this.originalUrl; // Media url can be played using html video. video.addEventListener('loadedmetadata', () => { const videoHeight = video.videoHeight; const videoWidth = video.videoWidth; const widthProportion = (videoWidth - videoHeight); const isLandscape = widthProportion > 0; // Store dimensions of the raw video. this.mediaDimensions = { width: videoWidth, height: videoHeight, }; // Set the media preview based on the media dimensions. if (isLandscape) { video.width = box.offsetWidth; } else { video.height = box.offsetHeight; } const height = this.root.querySelector(Selectors.EMBED.elements.height); const width = this.root.querySelector(Selectors.EMBED.elements.width); if (height.value === '' && width.value === '') { height.value = videoHeight; width.value = videoWidth; } // Size checking and adjustment. if (videoHeight === parseInt(height.value) && videoWidth === parseInt(width.value)) { this.currentWidth = this.mediaDimensions.width; this.currentHeight = this.mediaDimensions.height; this.sizeChecked('original'); } else { this.currentWidth = parseInt(width.value); this.currentHeight = parseInt(height.value); this.sizeChecked('custom'); } }); video.controls = true; if (this.media.poster) { previewArea.setAttribute('data-media-poster', this.media.poster); if (!video.classList.contains('w-100')) { video.classList.add('w-100'); } video.poster = this.media.poster; } video.load(); previewArea.setAttribute('data-media-type', 'video'); previewArea.innerHTML = video.outerHTML; notifyFilterContentUpdated(previewArea); } else if (this.mediaType === 'audio') { const audio = document.createElement('audio'); audio.src = this.originalUrl; audio.controls = true; audio.load(); previewArea.setAttribute('data-media-type', 'audio'); previewArea.innerHTML = audio.outerHTML; notifyFilterContentUpdated(previewArea); } else { // Show warning notification. const urlWarningLabelEle = this.root.querySelector(Selectors.EMBED.elements.urlWarning); urlWarningLabelEle.innerHTML = await getString('medianotavailabledesc', component, this.originalUrl); showElements(Selectors.EMBED.elements.urlWarning, this.root); // Stop the spinner. stopMediaLoading(this.root, Selectors.EMBED.type); // Reset the upload form. (new EmbedHandler(this)).resetUploadForm(); return; } // Stop the loader and display back the body template when the media is loaded. stopMediaLoading(this.root, Selectors.EMBED.type); showElements(Selectors.EMBED.elements.mediaDetailsBody, this.root); // Set the media name/title. this.root.querySelector(Selectors.EMBED.elements.title).value = this.setMediaTitle(); }; /** * Set media name/title. * * @returns {string} */ setMediaTitle = () => { // Getting and setting up media title/name. let fileName = null; if (['video', 'audio'].includes(this.mediaType)) { fileName = getFileName(this.originalUrl); // Get original filename. } else if (this.fetchedMediaLinkTitle) { fileName = this.fetchedMediaLinkTitle; } else { fileName = this.originalUrl; } if (this.isUpdating) { if (!this.newMediaLink) { fileName = this.mediaTitle; // Title from the selected media. } } return decodeURI(fileName); }; /** * Deletes the media after confirming with the user and loads the insert media page. */ deleteMedia = () => { Notification.deleteCancelPromise( getString('deletemedia', component), getString('deletemediawarning', component), ).then(() => { // Reset media upload form. (new EmbedHandler(this)).resetUploadForm(); // Delete any selected media mediaData. delete this.mediaData; return; }).catch(error => { window.console.log(error); }); }; /** * Delete embedded media thumbnail. */ deleteEmbeddedThumbnail = () => { Notification.deleteCancelPromise( getString('deleteembeddedthumbnail', component), getString('deleteembeddedthumbnailwarning', component), ).then(async() => { if (this.mediaType === 'video') { const video = this.root.querySelector('video'); if (video) { video.removeAttribute('poster'); const preview = this.root.querySelector(Selectors.EMBED.elements.mediaPreviewContainer); preview.removeAttribute('data-media-poster'); } } const deleteCustomThumbnail = this.root.querySelector(Selectors.EMBED.actions.deleteCustomThumbnail); deleteCustomThumbnail.remove(); const uploadCustomThumbnail = this.root.querySelector(Selectors.EMBED.actions.uploadCustomThumbnail); uploadCustomThumbnail.textContent = await getString('uploadthumbnail', component); return; }).catch(error => { window.console.log(error); }); }; /** * Shows the insert thumbnail dialogue. */ showUploadThumbnail = async() => { const uploadThumbnailModal = await EmbedModal.create({ large: true, templateContext: {elementid: this.editor.getElement().id}, }); const root = uploadThumbnailModal.getRoot()[0]; // Get selected media metadata. const mediaData = getEmbeddedMediaDetails(this); mediaData.isUpdating = this.isUpdating; const embedHandler = new EmbedHandler(this); embedHandler.loadInsertThumbnailTemplatePromise( insertMediaThumbnailTemplateContext(this), // Get template context for creating media thumbnail. {root, uploadThumbnailModal}, // Required root elements. await embedHandler.getMediaTemplateContext(mediaData) // Get current media data. ); }; /** * Only registers event listeners for new loaded elements in embed preview modal. */ registerMediaDetailsEventListeners = async() => { // Handle the original size when selected. const sizeOriginalEle = this.root.querySelector(Selectors.EMBED.elements.sizeOriginal); if (sizeOriginalEle) { sizeOriginalEle.addEventListener('change', () => { this.sizeChecked('original'); }); } // Handle the custom size when selected. const sizeCustomEle = this.root.querySelector(Selectors.EMBED.elements.sizeCustom); if (sizeCustomEle) { sizeCustomEle.addEventListener('change', () => { this.sizeChecked('custom'); }); } const widthEle = this.root.querySelector(Selectors.EMBED.elements.width); const heightEle = this.root.querySelector(Selectors.EMBED.elements.height); // Handle the custom with size when inputted. if (widthEle) { widthEle.addEventListener('input', () => { if (this.mediaType === 'link') { // Let's apply the 16:9 aspect ratio if it's a link media type. heightEle.value = Math.round(widthEle.value / this.linkMediaAspectRatio); } else { // Avoid empty value. widthEle.value = widthEle.value === "" ? 0 : Number(widthEle.value); this.autoAdjustSize(); } }); } // Handle the custom height size when inputted. if (heightEle) { heightEle.addEventListener('input', () => { if (this.mediaType === 'link') { // Let's apply the 16:9 aspect ratio if it's a link media type. widthEle.value = Math.round(heightEle.value * this.linkMediaAspectRatio); } else { // Avoid empty value. heightEle.value = heightEle.value === "" ? 0 : Number(heightEle.value); this.autoAdjustSize(true); } }); } // Handle media preview delete. const deleteMedia = this.root.querySelector(Selectors.EMBED.actions.deleteMedia); if (deleteMedia) { deleteMedia.addEventListener('click', (e) => { e.preventDefault(); this.deleteMedia(); }); } // Show subtitles and captions settings. const showSubtitleCaption = this.root.querySelector(Selectors.EMBED.actions.showSubtitleCaption); if (showSubtitleCaption) { showSubtitleCaption.addEventListener('click', (e) => { e.preventDefault(); hideElements([ Selectors.EMBED.actions.showSubtitleCaption, Selectors.EMBED.actions.cancelMediaDetails, Selectors.EMBED.elements.mediaDetailsBody, ], this.root); showElements([ Selectors.EMBED.actions.backToMediaDetails, Selectors.EMBED.elements.mediaSubtitleCaptionBody, ], this.root); }); } // Back to media preview. const backToMediaDetails = this.root.querySelector(Selectors.EMBED.actions.backToMediaDetails); if (backToMediaDetails) { backToMediaDetails.addEventListener('click', () => { hideElements([ Selectors.EMBED.actions.backToMediaDetails, Selectors.EMBED.elements.mediaSubtitleCaptionBody, ], this.root); showElements([ Selectors.EMBED.actions.showSubtitleCaption, Selectors.EMBED.actions.cancelMediaDetails, Selectors.EMBED.elements.mediaDetailsBody, ], this.root); }); } // Handles upload media thumbnail. const uploadCustomThumbnail = this.root.querySelector(Selectors.EMBED.actions.uploadCustomThumbnail); if (uploadCustomThumbnail) { uploadCustomThumbnail.addEventListener('click', () => { this.showUploadThumbnail(); }); } // Handles delete media thumbnail. const deleteCustomThumbnail = this.root.querySelector(Selectors.EMBED.actions.deleteCustomThumbnail); if (deleteCustomThumbnail) { deleteCustomThumbnail.addEventListener('click', () => { this.deleteEmbeddedThumbnail(); }); } // Handles language track selection. const langTracks = this.root.querySelectorAll(Selectors.EMBED.elements.trackLang); if (langTracks) { langTracks.forEach((dropdown) => { const defaultVal = dropdown.getAttribute('data-value'); if (defaultVal) { Array.from(dropdown.options).some(option => { // Check if srclang in track is a language code like "en" // or language name like "English" prior to MDL-85159. if (option.dataset.languageCode === defaultVal || option.value === defaultVal) { option.selected = true; return true; } return false; }); } }); } }; } PK/<\A""amd/src/embed/embedinsert.jsnu[// 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 upload class. * * This handles the embed upload using url, drag-drop and repositories. * * @module tiny_media/embed/embedinsert * @copyright 2024 Stevani Andolo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import {prefetchStrings} from 'core/prefetch'; import {getStrings} from 'core/str'; import {component} from "../common"; import { setPropertiesFromData, startMediaLoading, stopMediaLoading, showElements, } from '../helpers'; import Selectors from "../selectors"; import Dropzone from 'core/dropzone'; import uploadFile from 'editor_tiny/uploader'; import {EmbedHandler} from './embedhandler'; import { getMediaTitle, mediaDetailsTemplateContext, checkMediaType, fetchPreview, } from './embedhelpers'; import {EmbedPreview} from './embedpreview'; prefetchStrings(component, [ 'insertmedia', 'addmediafilesdrop', 'uploading', 'loadingmedia', ]); export class EmbedInsert { constructor(data) { setPropertiesFromData(this, data); // Creates dynamic properties based on "data" param. } /** * Init the dropzone and lang strings. */ init = async() => { const langStringKeys = [ 'insertmedia', 'addmediafilesdrop', 'uploading', 'loadingmedia', ]; const langStringValues = await getStrings([...langStringKeys].map((key) => ({key, component}))); this.langStrings = Object.fromEntries(langStringKeys.map((key, index) => [key, langStringValues[index]])); this.currentModal.setTitle(this.langStrings.insertmedia); // Let's init the dropzone if canShowDropZone is true and mediaType is null. if (this.canShowDropZone && !this.mediaType) { const dropZoneEle = document.querySelector(Selectors.EMBED.elements.dropzoneContainer); const dropZone = new Dropzone( dropZoneEle, this.acceptedMediaTypes, files => { this.handleUploadedFile(files); } ); dropZone.setLabel(this.langStrings.addmediafilesdrop); dropZone.init(); } }; /** * Loads and displays a preview media based on the provided URL, and handles media loading events. * * @param {string} url - The URL of the media to load and display. */ loadMediaPreview = async(url) => { const parsedUrl = new URL(url); this.fetchedMediaLinkTitle = await getMediaTitle(url, this); // The hash in a URL can cause issues. Remove it. this.originalUrl = parsedUrl.hash ? parsedUrl.toString().split('#')[0] : parsedUrl.toString(); if (this.newMediaLink) { // Media added using url input. this.filteredContent = await fetchPreview(this.originalUrl, this.contextId); if (!this.mediaType) { // If the filtered content is not empty, it means Moodle's filter recognized it as external media. if (this.filteredContent && this.filteredContent.trim() !== '') { this.mediaType = 'link'; } else if (this.fetchedMediaLinkTitle) { // It means the url points to a physical media file with a hash-based title. // Case-insensitive regex for video tag. const videoRegex = /]*>.*<\/video>/i; // Case-insensitive regex for audio tag. const audioRegex = /]*>.*<\/audio>/i; if (videoRegex.test(this.filteredContent)) { this.mediaType = 'video'; } else if (audioRegex.test(this.filteredContent)) { this.mediaType = 'audio'; } } } // Process the media preview. this.processMediaPreview(); } else { // Media added using dropzone or repositories. this.mediaType ??= await checkMediaType(url); // If checkMediaType returns null, try fetching preview to detect external media providers. if (!this.mediaType) { this.filteredContent = await fetchPreview(this.originalUrl, this.contextId); // If the filtered content is not empty, it means Moodle's filter recognized it as external media. if (this.filteredContent && this.filteredContent.trim() !== '') { this.mediaType = 'link'; } } // Process the media preview. this.processMediaPreview(); } }; /** * Process the media preview. */ processMediaPreview = async() => { // Let's combine the props. setPropertiesFromData( this, await (new EmbedHandler(this)).getMediaTemplateContext() ); // Construct templateContext for embed preview. const templateContext = await mediaDetailsTemplateContext(this); if (this.isUpdating && !this.newMediaLink) { // Will be used to set the media title if it's in update state. this.mediaTitle = templateContext.media.title; } // Load the media details and preview of the selected media. (new EmbedHandler(this)).loadMediaDetails(new EmbedPreview(this), templateContext); }; /** * 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 loaderIconState = root.querySelector(Selectors.EMBED.elements.loaderIconContainer + ' div'); loaderIconState.innerHTML = (progress !== null) ? `${langStrings.uploading} ${Math.round(progress)}%` : langStrings.loadingmedia; }; /** * Handles media preview on file picker callback. * * @param {object} params Object of uploaded file */ filePickerCallback = (params) => { if (params.url) { if (this.mediaType) { // Set mediaType to "null" if it started with viewing embedded link, otherwise it will not be consistent. this.mediaType = null; } // Flag as new file upload. this.newFileUpload = true; // Load the media preview. this.loadMediaPreview(params.url); } }; /** * 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 { startMediaLoading(this.root, Selectors.EMBED.type); const fileURL = await uploadFile(this.editor, 'media', 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.EMBED.elements.urlWarning); urlWarningLabelEle.innerHTML = error.error !== undefined ? error.error : error; showElements(Selectors.EMBED.elements.urlWarning, this.root); stopMediaLoading(this.root, Selectors.EMBED.type); } }; } PK/<\PWW%amd/src/embed/embedthumbnailinsert.jsnu[// 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 thumbnail upload class. * * This handles the embed thumbnail upload using drag-drop. * * @module tiny_media/embed/embedthumbnailinsert * @copyright 2024 Stevani Andolo * @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 { showElements, startMediaLoading, stopMediaLoading, setPropertiesFromData, body, footer, } from '../helpers'; import {EmbedThumbnailPreview} from './embedthumbnailpreview'; import {EmbedHandler} from './embedhandler'; import {displayFilepicker} from 'editor_tiny/utils'; prefetchStrings(component, [ 'insertmediathumbnail', 'uploading', 'loadingembedthumbnail', 'addmediathumbnaildrop', ]); export class EmbedThumbnailInsert { constructor(data) { setPropertiesFromData(this, data); // Creates dynamic properties based on "data" param. } /** * Init the dropzone and lang strings. * * @param {object} mediaData Object of selected media data */ init = async(mediaData) => { this.mediaData = mediaData; // Current selected media data passed from embedPreview. const langStringKeys = [ 'insertmediathumbnail', 'uploading', 'loadingembedthumbnail', 'addmediathumbnaildrop', ]; const langStringvalues = await getStrings([...langStringKeys].map((key) => ({key, component}))); this.langStrings = Object.fromEntries(langStringKeys.map((key, index) => [key, langStringvalues[index]])); this.currentModal.uploadThumbnailModal.setTitle(this.langStrings.insertmediathumbnail); // Let's init the dropzone if canShowDropZone is true. if (this.canShowDropZone) { const dropZoneEle = document.querySelector(Selectors.EMBED.elements.dropzoneContainer); const dropZone = new Dropzone( dropZoneEle, this.acceptedImageTypes, files => { this.handleUploadedFile(files); } ); dropZone.setLabel(this.langStrings.addmediathumbnaildrop); dropZone.init(); } this.registerInsertMediaThumbnailEvents(this.thumbnailModalRoot); }; /** * Load and display a preview thumbnail based on the provided URL, and handles thumbnail loading events. * * @param {string} url - The URL of the thumbnail to load and display. */ loadPreviewThumbnail = (url) => { this.media.poster = url; let templateContext = { bodyTemplate: Selectors.EMBED.template.body.mediaThumbnailBody, footerTemplate: Selectors.EMBED.template.footer.mediaThumbnailFooter, selector: Selectors.EMBED.type, }; Promise.all([body(templateContext, this.thumbnailModalRoot), footer(templateContext, this.thumbnailModalRoot)]) .then(() => { const mediaThumbnail = new EmbedThumbnailPreview(this); mediaThumbnail.init(this.mediaData); return; }) .catch(error => { window.console.log(error); }); }; /** * Handles media preview on file picker callback. * * @param {object} params Object of uploaded file */ filePickerCallback = (params) => { if (params.url) { this.loadPreviewThumbnail(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). */ updateLoaderIcon = (root, langStrings, progress = null) => { const loaderIconState = root.querySelector(Selectors.EMBED.elements.loaderIconContainer + ' div'); loaderIconState.innerHTML = (progress !== null) ? `${langStrings.uploading} ${Math.round(progress)}%` : langStrings.loadingembedthumbnail; }; /** * Handles changes in the media URL input field and loads a preview of the media if the URL has changed. */ urlChanged() { const url = this.thumbnailModalRoot.querySelector(Selectors.EMBED.elements.fromUrl).value; if (url && url !== this.currentUrl) { this.loadPreviewThumbnail(url); } } /** * 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 { startMediaLoading(this.thumbnailModalRoot, Selectors.EMBED.type); const fileURL = await uploadFile(this.editor, 'image', files[0], files[0].name, (progress) => { this.updateLoaderIcon(this.thumbnailModalRoot, this.langStrings, progress); }); // Set the loader icon content to "loading" after the file upload completes. this.updateLoaderIcon(this.thumbnailModalRoot, this.langStrings); this.filePickerCallback({url: fileURL}); } catch (error) { // Handle the error. const urlWarningLabelEle = this.thumbnailModalRoot.querySelector(Selectors.EMBED.elements.urlWarning); urlWarningLabelEle.innerHTML = error.error !== undefined ? error.error : error; showElements(Selectors.EMBED.elements.urlWarning, this.thumbnailModalRoot); stopMediaLoading(this.thumbnailModalRoot, Selectors.EMBED.type); } }; /** * Registers events for insert thumbnail modal. * * @param {HTMLElement} root - The root element containing the loader icon. */ registerInsertMediaThumbnailEvents = (root) => { const urlEle = root.querySelector(Selectors.EMBED.elements.fromUrl); if (urlEle) { urlEle.addEventListener('input', () => { (new EmbedHandler(this)).toggleUrlButton(urlEle, this.thumbnailModalRoot); }); } // Handles add media url. const addUrlEle = root.querySelector(Selectors.EMBED.actions.addUrl); if (addUrlEle) { addUrlEle.addEventListener('click', () => { startMediaLoading(this.thumbnailModalRoot, Selectors.EMBED.type); this.urlChanged(); }); } // Handle repository browsing. const imageBrowser = root.querySelector(Selectors.IMAGE.actions.imageBrowser); if (imageBrowser) { imageBrowser.addEventListener('click', async(e) => { e.preventDefault(); const params = await displayFilepicker(this.editor, 'image'); this.filePickerCallback(params); }); } }; } PK/<\WC&amd/src/embed/embedthumbnailpreview.jsnu[// 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 thumbnail preview class. * * This handles: * - Embed thumbnail preview. * - Delete thumbnail preview. * - Update the embed preview details with new thumbnail. * * @module tiny_media/embed/mediathumbnail * @copyright 2024 Stevani Andolo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Selectors from '../selectors'; import {getString} from 'core/str'; import { sourceTypeChecked, setPropertiesFromData, showElements, stopMediaLoading, } from '../helpers'; import Notification from 'core/notification'; import {EmbedPreview} from './embedpreview'; import {mediaDetailsTemplateContext} from './embedhelpers'; import {EmbedHandler} from './embedhandler'; import {component} from '../common'; export class EmbedThumbnailPreview { constructor(data) { setPropertiesFromData(this, data); // Creates dynamic properties based on "data" param. } /** * Init the media thumbnail preview. * * @param {object} mediaData Object of selected media data */ init = (mediaData) => { this.mediaData = mediaData; this.currentModal.uploadThumbnailModal.setTitle(getString('thumbnail', component)); sourceTypeChecked({ source: this.media.poster, root: this.thumbnailModalRoot, fileNameSelector: Selectors.EMBED.elements.fileNameLabel, }); this.setThumbnailSource(); this.registerMediaThumbnailEventListeners(); }; /** * Sets media thumbnail source. */ setThumbnailSource = () => { const thumbnailPreview = this.thumbnailModalRoot.querySelector(Selectors.EMBED.elements.thumbnailPreview); thumbnailPreview.src = this.media.poster; thumbnailPreview.addEventListener('error', async() => { // Show warning notification. const urlWarningLabelEle = this.thumbnailModalRoot.querySelector(Selectors.EMBED.elements.urlWarning); urlWarningLabelEle.innerHTML = await getString('imageurlrequired', component); showElements(Selectors.EMBED.elements.urlWarning, this.thumbnailModalRoot); // Stop the spinner. stopMediaLoading(this.thumbnailModalRoot, Selectors.EMBED.type); // Reset the upload form. (new EmbedHandler(this)).resetUploadForm(false); }); thumbnailPreview.addEventListener('load', () => { this.mediaData.media.poster = this.media.poster; this.media = this.mediaData.media; stopMediaLoading(this.thumbnailModalRoot, Selectors.EMBED.type); }); }; /** * Deletes the media after confirming with the user and loads the insert media page. */ deleteMedia = () => { Notification.deleteCancelPromise( getString('deletemediathumbnail', component), getString('deletemediathumbnailwarning', component) ).then(() => { (new EmbedHandler(this)).resetUploadForm(false); return; }).catch(error => { window.console.log(error); }); }; /** * Loads and displays a media preview with thumbnail. */ loadPreviewMediaThumbnail = async() => { (new EmbedHandler(this)).loadMediaDetails(new EmbedPreview(this), await mediaDetailsTemplateContext(this)) .then(() => { // Close the thumbnail upload modal once media details have been loaded. this.currentModal.uploadThumbnailModal.destroy(); const currentModal = this.currentModal.insertMediaModal; this.currentModal = currentModal.insertMediaModal; delete this.mediaData; return; }).catch(error => { window.console.log(error); }); }; /** * Only registers event listeners for new loaded elements in mediaThumbnail. */ registerMediaThumbnailEventListeners = () => { // Handles delete thumbnail. const deleteMedia = this.thumbnailModalRoot.querySelector(Selectors.EMBED.actions.deleteThumbnail); if (deleteMedia) { deleteMedia.addEventListener('click', (e) => { e.preventDefault(); this.deleteMedia(); }); } // Handles setting the media poster. const setPoster = this.thumbnailModalRoot.querySelector(Selectors.EMBED.actions.setPoster); if (setPoster) { setPoster.addEventListener('click', () => { this.loadPreviewMediaThumbnail(); }); } }; } PK/<\R†%%amd/src/embed/embedhelpers.jsnu[// 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 helpers. * * This provides easy access to any classes without instantiating a new object. * * @module tiny_media/embed/embedhelpers * @copyright 2024 Stevani Andolo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Selectors from '../selectors'; import {getStrings} from 'core/str'; import {component} from "../common"; import {getCurrentLanguage, getMoodleLang} from 'editor_tiny/options'; import Ajax from 'core/ajax'; import {getFileName} from '../helpers'; /** * Return template context for insert media. * * @param {object} props * @returns {object} */ export const insertMediaTemplateContext = (props) => { return { mediaType: props.mediaType, showDropzone: props.canShowDropZone, showFilePicker: props.canShowFilePicker, fileType: 'audio/video', }; }; /** * Return template context for insert media. * * @param {object} props * @returns {object} */ export const insertMediaThumbnailTemplateContext = (props) => { return { elementid: props.editor.id, showDropzone: props.canShowDropZone, showImageFilePicker: props.canShowImageFilePicker, bodyTemplate: Selectors.EMBED.template.body.insertMediaBody, footerTemplate: Selectors.EMBED.template.footer.insertMediaFooter, fileType: 'image', selector: Selectors.EMBED.type, }; }; /** * Return selected media type and element. * * @param {editor} editor * @returns {Array} */ export const getSelectedMediaElement = (editor) => { let mediaType = null; let selectedMedia = null; const mediaElm = editor.selection.getNode(); if (!mediaElm) { mediaType = null; selectedMedia = null; } else if (mediaElm.nodeName.toLowerCase() === 'video' || mediaElm.nodeName.toLowerCase() === 'audio') { mediaType = mediaElm.nodeName.toLowerCase(); selectedMedia = mediaElm; } else if (mediaElm.querySelector('video')) { mediaType = 'video'; selectedMedia = mediaElm.querySelector('video'); } else if (mediaElm.querySelector('audio')) { mediaType = 'audio'; selectedMedia = mediaElm.querySelector('audio'); } else if (mediaElm.nodeName.toLowerCase() === 'a') { selectedMedia = mediaElm; } return [mediaType, selectedMedia]; }; /** * Returns result of media filtering. * * @param {string} url * @param {string} contextId * @returns {string} */ export const fetchPreview = async(url, contextId) => { const request = { methodname: 'tiny_media_preview', args: { contextid: contextId, // Use the system one. content: url, } }; const responseObj = await Ajax.call([request])[0]; return responseObj.content; }; /** * Returns media type. * * @param {string} url * @returns {string|null} */ export const checkMediaType = async(url) => { try { const response = await fetch(url, {method: 'HEAD'}); const contentType = response.headers.get('Content-type'); if (!contentType) { return null; } if (contentType.startsWith('video/')) { return 'video'; } else if (contentType.startsWith('audio/')) { return 'audio'; } return null; } catch (e) { return null; } }; /** * Returns media title. * * @param {string} url * @param {object} props * @returns {string|null} String of media title. */ export const getMediaTitle = async(url, props) => { const parsedUrl = new URL(url); // Try get the title via the file name. const pathName = parsedUrl.pathname; const extension = pathName.includes('.') ? pathName.split('.').pop() : null; if (extension && props.acceptedMediaTypes.includes(`.${extension}`)) { return getFileName(url); } // Try and get the title from the value after the '#'. const hashTitle = parsedUrl.hash ? parsedUrl.hash.substring(1) : null; if (hashTitle) { return hashTitle; } return null; }; /** * Return template context for media details. * * @param {object} props * @returns {object} */ export const mediaDetailsTemplateContext = async(props) => { const context = { bodyTemplate: Selectors.EMBED.template.body.mediaDetailsBody, footerTemplate: Selectors.EMBED.template.footer.mediaDetailsFooter, isLink: (props.mediaType === 'link'), isVideo: (props.mediaType === 'video'), showControl: (props.mediaType === 'video' || props.mediaType === 'audio'), isUpdating: props.isUpdating, isNewFileOrLinkUpload: (props.newMediaLink || props.newFileUpload), selector: Selectors.EMBED.type, }; return {...context, ...props}; }; /** * Get help strings. * * @returns {object} */ export const getHelpStrings = async() => { const [ subtitles, captions, descriptions, chapters, metadata, customsize, linkcustomsize, ] = await getStrings([ 'subtitles_help', 'captions_help', 'descriptions_help', 'chapters_help', 'metadata_help', 'customsize_help', 'linkcustomsize_help', ].map((key) => ({ key, component, }))); return { subtitles, captions, descriptions, chapters, metadata, customsize, linkcustomsize, }; }; /** * Get current moodle languages. * * @param {editor} editor * @returns {object} */ export const prepareMoodleLang = (editor) => { const moodleLangs = getMoodleLang(editor); const currentLanguage = getCurrentLanguage(editor); const installed = Object.entries(moodleLangs.installed).map(([code, lang]) => ({ lang, code, "default": code === currentLanguage, })); const available = Object.entries(moodleLangs.available).map(([code, lang]) => ({ lang, code, "default": code === currentLanguage, })); return { installed, available, }; }; /** * Return moodle lang. * * @param {string} subtitleLang * @param {editor} editor * @returns {object|null} */ export const getMoodleLangObj = (subtitleLang, editor) => { const {available} = getMoodleLang(editor); if (available[subtitleLang]) { return { lang: subtitleLang, code: available[subtitleLang], }; } return null; }; /** * Get media data from the inserted media. * * @param {object} props * @returns {object} */ export const getEmbeddedMediaDetails = (props) => { const tracks = { subtitles: [], captions: [], descriptions: [], chapters: [], metadata: [] }; const mediaMetadata = props.root.querySelectorAll(Selectors.EMBED.elements.mediaMetadataTabPane); mediaMetadata.forEach(metaData => { const trackElements = metaData.querySelectorAll(Selectors.EMBED.elements.track); trackElements.forEach(track => { tracks[metaData.dataset.trackKind].push({ src: track.querySelector(Selectors.EMBED.elements.url).value, srclang: track.querySelector(Selectors.EMBED.elements.trackLang).value, label: track.querySelector(Selectors.EMBED.elements.trackLabel).value, defaultTrack: track.querySelector(Selectors.EMBED.elements.trackDefault).checked, }); }); }); const querySelector = (element) => props.root.querySelector(element); const mediaDataProps = {}; mediaDataProps.media = { type: props.mediaType, sources: props.media, poster: props.media.poster ?? null, title: querySelector(Selectors.EMBED.elements.title).value, width: querySelector(Selectors.EMBED.elements.width).value, height: querySelector(Selectors.EMBED.elements.height).value, autoplay: querySelector(Selectors.EMBED.elements.mediaAutoplay).checked, loop: querySelector(Selectors.EMBED.elements.mediaLoop).checked, muted: querySelector(Selectors.EMBED.elements.mediaMute).checked, controls: querySelector(Selectors.EMBED.elements.mediaControl).checked, tracks, }; mediaDataProps.link = false; return mediaDataProps; }; /** * Check for video/audio attributes. * * @param {HTMLElement} elem * @param {string} attr Attribute name * @returns {boolean} */ export const hasAudioVideoAttr = (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) === '')); }; PK/<\$/STSTamd/src/embed/embedhandler.jsnu[// 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 handler class. * * This handles anything that embed requires like: * - Calling the media preview in embedPreview. * - Loading the embed insert. * - Getting selected media data. * - Handles url and repository uploads. * - Reset embed insert when embed preview is deleted. * - Handles media embedding into tiny and etc. * * @module tiny_media/embed/embedhandler * @copyright 2024 Stevani Andolo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Selectors from "../selectors"; import {EmbedInsert} from './embedinsert'; import { body, footer, setPropertiesFromData, isValidUrl, stopMediaLoading, startMediaLoading, } from '../helpers'; import * as ModalEvents from 'core/modal_events'; import {displayFilepicker} from 'editor_tiny/utils'; import { insertMediaTemplateContext, getHelpStrings, prepareMoodleLang, getMoodleLangObj, hasAudioVideoAttr, insertMediaThumbnailTemplateContext, } from "./embedhelpers"; import Templates from 'core/templates'; import {EmbedThumbnailInsert} from './embedthumbnailinsert'; export class EmbedHandler { constructor(data) { setPropertiesFromData(this, data); // Creates dynamic properties based on "data" param. } /** * Load the media insert dialogue. * * @param {object} templateContext Object template context */ loadTemplatePromise = (templateContext) => { templateContext.elementid = this.editor.id; templateContext.bodyTemplate = Selectors.EMBED.template.body.insertMediaBody; templateContext.footerTemplate = Selectors.EMBED.template.footer.insertMediaFooter; templateContext.selector = Selectors.EMBED.type; Promise.all([body(templateContext, this.root), footer(templateContext, this.root)]) .then(() => { (new EmbedInsert(this)).init(); return; }) .catch(error => { window.console.log(error); }); }; /** * Load the media thumbnail insert dialogue. * * @param {object} templateContext Object template context * @param {HTMLElement} root * @param {object} mediaData */ loadInsertThumbnailTemplatePromise = async(templateContext, root, mediaData) => { Promise.all([body(templateContext, root.root), footer(templateContext, root.root)]) .then(() => { if (!this.currentModal.insertMediaModal) { this.currentModal.insertMediaModal = this.currentModal; } if (root.uploadThumbnailModal) { this.currentModal.uploadThumbnailModal = root.uploadThumbnailModal; } this.thumbnailModalRoot = root.root; (new EmbedThumbnailInsert(this)).init(mediaData); return; }) .catch(error => { window.console.log(error); }); }; /** * Loads the media preview dialogue. * * @param {object} embedPreview Object of embedPreview * @param {object} templateContext Object of template context */ loadMediaDetails = async(embedPreview, templateContext) => { Promise.all([body(templateContext, this.root), footer(templateContext, this.root)]) .then(() => { embedPreview.init(); return; }) .catch(error => { stopMediaLoading(this.root, Selectors.EMBED.type); window.console.log(error); }); }; /** * Reset the media/thumbnail insert modal form. * * @param {boolean} isMediaInsert Is current state media insert or thumbnail insert? */ resetUploadForm = (isMediaInsert = true) => { if (isMediaInsert) { this.newMediaLink = false; this.fetchedMediaLinkTitle = null; this.resetCurrentMediaData(); this.loadTemplatePromise(insertMediaTemplateContext(this)); } else { this.loadInsertThumbnailTemplatePromise( insertMediaThumbnailTemplateContext(this), // Get template context for creating media thumbnail. {root: this.thumbnailModalRoot}, // Required root elements. this.mediaData // Get current media data. ); } }; /** * Get selected media data. * * @returns {null|object} */ getMediaProperties = () => { const media = this.selectedMedia; if (!media) { return null; } const tracks = { subtitles: [], captions: [], descriptions: [], chapters: [], metadata: [] }; const sources = []; media.querySelectorAll('track').forEach((track) => { tracks[track.getAttribute('kind')].push({ src: track.getAttribute('src'), srclang: track.getAttribute('srclang'), label: track.getAttribute('label'), defaultTrack: hasAudioVideoAttr(track, 'default') }); }); media.querySelectorAll('source').forEach((source) => { sources.push(source.src); }); const title = media.getAttribute('title') ?? media.textContent; return { type: this.mediaType, sources, poster: media.getAttribute('poster'), title: title ? title.trim() : false, width: media.getAttribute('width'), height: media.getAttribute('height'), autoplay: hasAudioVideoAttr(media, 'autoplay'), loop: hasAudioVideoAttr(media, 'loop'), muted: hasAudioVideoAttr(media, 'muted'), controls: hasAudioVideoAttr(media, 'controls'), tracks, }; }; /** * Get selected media data. * * @returns {object} */ getCurrentEmbedData = () => { const properties = this.getMediaProperties(); if (!properties || this.newMediaLink) { return {media: {}}; } const processedProperties = {}; processedProperties.media = properties; processedProperties.link = false; return processedProperties; }; /** * Get help strings for media subtitles and captions. * * @returns {null|object} */ getHelpStrings = async() => { if (!this.helpStrings) { this.helpStrings = await getHelpStrings(); } return this.helpStrings; }; /** * Set template context for insert media dialogue. * * @param {object} data Object of media data * @returns {object} */ getTemplateContext = async(data) => { const languages = prepareMoodleLang(this.editor); 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, showFilePickerTrack: this.canShowFilePickerTrack, langsInstalled: languages.installed, langsAvailable: languages.available, media: true, isUpdating: this.isUpdating, }, data, helpIcons); }; /** * Set and get media template context. * * @param {null|object} data Null or object of media data * @returns {Promise} A promise that resolves template context. */ getMediaTemplateContext = async(data = null) => { if (!data) { data = Object.assign({}, this.getCurrentEmbedData()); } else { if (data.hasOwnProperty('isUpdating')) { this.isUpdating = data.isUpdating; } else { this.isUpdating = Object.keys(data).length > 1; } } return await this.getTemplateContext(data); }; /** * Handles changes in the media URL input field and loads a preview of the media if the URL has changed. */ urlChanged() { const url = this.root.querySelector(Selectors.EMBED.elements.fromUrl).value; if (url && url !== this.currentUrl) { // Set to null on new url change. this.mediaType = null; // Flag as new media link insert. this.newMediaLink = true; this.loadMediaPreview(url); } } /** * Load the media preview dialogue. * * @param {string} url String of media url */ loadMediaPreview = (url) => { (new EmbedInsert(this)).loadMediaPreview(url); }; /** * Callback for file picker that previews the media or add the captions and subtitles. * * @param {object} params Object of media url and etc * @param {html} element Selected element. * @param {string} fpType Caption type. */ trackFilePickerCallback(params, element, fpType) { if (params.url !== '') { const tabPane = element.closest('.tab-pane'); if (tabPane) { element.closest(Selectors.EMBED.elements.source).querySelector(Selectors.EMBED.elements.url).value = params.url; 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 = getMoodleLangObj(subtitleLang, this.editor); 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; } } } else { // Flag as new file upload. this.newFileUpload = true; this.resetCurrentMediaData(); this.loadMediaPreview(params.url); } } } /** * Reset current media data. */ resetCurrentMediaData = () => { // Reset the value of the following props. this.media = {}; this.mediaType = null; this.selectedMedia = null; }; /** * Add new html track element. * * @param {html} element */ addTrackComponent(element) { 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); } /** * Remove added html track element. * * @param {html} element */ removeTrackComponent(element) { const sourceElement = element.closest(Selectors.EMBED.elements.track); sourceElement.remove(); } /** * Get picker type based on the selected element. * * @param {html} element Selected element * @returns {string} */ getFilePickerTypeFromElement = (element) => { if (element.closest(Selectors.EMBED.elements.posterSource)) { return 'image'; } if (element.closest(Selectors.EMBED.elements.trackSource)) { return 'subtitle'; } return 'media'; }; /** * Get captions/subtitles type. * * @param {html} tabPane * @returns {string} */ getTrackTypeFromTabPane = (tabPane) => { return tabPane.getAttribute('data-track-kind'); }; /** * Handle click events. * * @param {html} e Selected element */ clickHandler = async(e) => { const element = e.target; // Handle repository browsing. 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.trackFilePickerCallback(params, element, fpType); } // Handles add media url. const addUrlEle = e.target.closest(Selectors.EMBED.actions.addUrl); if (addUrlEle) { startMediaLoading(this.root, Selectors.EMBED.type); this.urlChanged(); } // Handles adding tracks. const addComponentTrackAction = element.closest(Selectors.EMBED.elements.track + ' .addcomponent'); if (addComponentTrackAction) { e.preventDefault(); this.addTrackComponent(element); } // Handles removing added tracks. 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('.tab-content') .querySelectorAll(Selectors.EMBED.elements.trackDefault) .forEach((select) => { if (select !== element && getKind(element) === getKind(select)) { select.checked = false; } }); } }; /** * Enables or disables the URL-related buttons in the footer based on the current URL and input value. * * @param {html} input Url input field * @param {object} root */ toggleUrlButton(input, root) { const url = input.value; const addUrl = root.querySelector(Selectors.EMBED.actions.addUrl); addUrl.disabled = !(url !== "" && isValidUrl(url)); } /** * Get media html to be inserted or updated into tiny. * * @param {html} form Selected element * @returns {string} String of html */ getMediaHTML = (form) => { this.mediaType = this.root.querySelector(Selectors.EMBED.elements.mediaPreviewContainer).dataset.mediaType; const tabContent = form.querySelector('.tab-content'); const callback = 'getMediaHTML' + this.mediaType[0].toUpperCase() + this.mediaType.substr(1); return this[callback](tabContent); }; /** * Get media as link. * * @returns {string} String of html. */ getMediaHTMLLink() { const mediaPreviewContainer = document.querySelector(Selectors.EMBED.elements.mediaPreviewContainer); const context = { name: document.querySelector(Selectors.EMBED.elements.title).value ?? mediaPreviewContainer.dataset.originalUrl, url: mediaPreviewContainer.dataset.originalUrl || false }; return context.url ? Templates.renderForPromise('tiny_media/embed/embed_media_link', context) : ''; } /** * Get media as video. * * @param {html} tab Selected element * @returns {string} String of html. */ getMediaHTMLVideo = (tab) => { const details = document.querySelector(Selectors.EMBED.elements.mediaDetailsBody); const context = this.getContextForMediaHTML(tab, details); context.width = details.querySelector(Selectors.EMBED.elements.width).value || false; context.height = details.querySelector(Selectors.EMBED.elements.height).value || false; const mediaPreviewContainer = details.querySelector(Selectors.EMBED.elements.mediaPreviewContainer); context.poster = mediaPreviewContainer.dataset.mediaPoster || false; return context.sources ? Templates.renderForPromise('tiny_media/embed/embed_media_video', context) : ''; }; /** * Get media as audio. * * @param {html} tab Selected element * @returns {string} String of html. */ getMediaHTMLAudio = (tab) => { const details = document.querySelector(Selectors.EMBED.elements.mediaDetailsBody); const context = this.getContextForMediaHTML(tab, details); return context.sources.length ? Templates.renderForPromise('tiny_media/embed/embed_media_audio', context) : ''; }; /** * Get previewed media data. * * @param {html} tab Selected element * @param {html} details Selected element * @returns {object} */ getContextForMediaHTML = (tab, details) => { const tracks = Array.from(tab.querySelectorAll(Selectors.EMBED.elements.track)).map(track => { const langTrack = track.querySelector(Selectors.EMBED.elements.trackLang); const selectedLangTrack = langTrack.options[langTrack.selectedIndex]; return { 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 || langTrack.value, srclang: selectedLangTrack.dataset.languageCode ?? false, defaultTrack: track.querySelector(Selectors.EMBED.elements.trackDefault).checked ? "true" : null }; }).filter((track) => !!track.track); const mediaPreviewContainer = details.querySelector(Selectors.EMBED.elements.mediaPreviewContainer); let sources = mediaPreviewContainer.dataset.originalUrl ?? null; // Let's check if media has more than one sources. if (this.alternativeSources) { // Always update the first item in this.alternativeSources to the new one. this.alternativeSources[0] = sources; // Override the sources to have all the updated sources. sources = this.alternativeSources; } const title = details.querySelector(Selectors.EMBED.elements.title).value; // Remove data-original-url attribute once it's extracted. mediaPreviewContainer.removeAttribute('data-original-url'); const templateContext = { sources, tracks, showControls: details.querySelector(Selectors.EMBED.elements.mediaControl).checked, autoplay: details.querySelector(Selectors.EMBED.elements.mediaAutoplay).checked, muted: details.querySelector(Selectors.EMBED.elements.mediaMute).checked, loop: details.querySelector(Selectors.EMBED.elements.mediaLoop).checked, title: title !== '' ? title.trim() : false, }; // Add description prop to templateContext if media type is "link". if (this.mediaType === 'link') { // Let's form an alternative title. templateContext.description = Array.isArray(sources) ? sources[0] : sources; } return templateContext; }; /** * Handle the insert/update media in tiny editor. * * @param {event} event * @param {object} modal Object of current modal */ handleDialogueSubmission = async(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); } } }; /** * Register insert media modal elements' events. */ registerEventListeners = async() => { // Handles click events for insert media modal. if (this.canShowFilePickerTrack) { this.root.addEventListener('click', this.clickHandler.bind(this)); } // Handles media adding using url input. this.root.addEventListener('input', (e) => { const urlEle = e.target.closest(Selectors.EMBED.elements.fromUrl); if (urlEle) { this.toggleUrlButton(urlEle, this.root); } }); // Destroy created modal when it's closed. this.modalRoot.on(ModalEvents.hidden, () => { this.currentModal.destroy(); }); // Handles media insert to editor. this.modalRoot.on(ModalEvents.save, this.handleDialogueSubmission.bind(this)); }; } PK/<\jc amd/src/manager.jsnu[// 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_media/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_media'), body: Templates.render('tiny_media/mm_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('mm-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/media/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(); } } PK/<\RWamd/src/selectors.jsnu[// 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_media/selectors * @copyright 2022 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ export default { IMAGE: { actions: { submit: '.tiny_image_urlentrysubmit', imageBrowser: '.openimagebrowser', addUrl: '.tiny_image_addurl', deleteImage: '.tiny_image_deleteicon', }, elements: { form: 'form.tiny_image_form', alignSettings: '.tiny_image_button', alt: '.tiny_image_altentry', altWarning: '.tiny_image_altwarning', height: '.tiny_image_heightentry', width: '.tiny_image_widthentry', url: '.tiny_image_urlentry', urlWarning: '.tiny_image_urlwarning', size: '.tiny_image_size', presentation: '.tiny_image_presentation', constrain: '.tiny_image_constrain', customStyle: '.tiny_image_customstyle', preview: '.tiny_image_preview', previewBox: '.tiny_image_preview_box', loaderIcon: '.tiny_image_loader', loaderIconContainer: '.tiny_image_loader_container', insertImage: '.tiny_image_insert_image', modalFooter: '.modal-footer', dropzoneContainer: '.tiny_image_dropzone_container', fileInput: '#tiny_image_fileinput', fileNameLabel: '.tiny_image_filename', sizeOriginal: '.tiny_image_sizeoriginal', sizeCustom: '.tiny_image_sizecustom', properties: '.tiny_image_properties', bodyTemplate: '.tiny_image_body_template', footerTemplate: '.tiny_image_footer_template', }, styles: { responsive: 'img-fluid', }, template: { body: { insertImageBody: 'tiny_media/insert_image_modal_insert', insertImageDetailsBody: 'tiny_media/insert_image_modal_details', }, footer: { insertImageFooter: 'tiny_media/insert_image_modal_insert_footer', insertImageDetailsFooter: 'tiny_media/insert_image_modal_details_footer', }, }, type: 'IMAGE', }, EMBED: { actions: { mediaBrowser: '.openmediabrowser', addUrl: '.tiny_media_add_url', deleteMedia: '.tiny_media_delete_icon', showSubtitleCaption: '[data-action="show-subtitle-caption"]', backToMediaDetails: '[data-action="back-to-media-details"]', uploadCustomThumbnail: '.upload-custom-thumbnail', deleteCustomThumbnail: '.delete-custom-thumbnail', deleteThumbnail: '.delete_tiny_thumbnail', setPoster: '[data-action="set-poster"]', mediaLinkAsAudio: '.link_as_audio', mediaLinkAsVideo: '.link_as_video', cancelMediaDetails: '[data-action="cancel"]', }, elements: { source: '.tiny_media_source', track: '.tiny_media_track', posterSource: '.tiny_media_poster_source', title: '.tiny_media_title_entry', url: '.tiny_media_source_url_entry', width: '.tiny_media_width_entry', height: '.tiny_media_height_entry', trackSource: '.tiny_media_track_source', trackLabel: '.tiny_media_track_label_entry', trackLang: '.tiny_media_track_lang_entry', trackDefault: '.tiny_media_track_default', mediaControl: '.tiny_media_controls', mediaAutoplay: '.tiny_media_autoplay', mediaMute: '.tiny_media_mute', mediaLoop: '.tiny_media_loop', bodyTemplate: '.tiny_media_body_template', footerTemplate: '.tiny_media_footer_template', dropzoneContainer: '.tiny_media_dropzone_container', fromUrl: '.tiny_media_from_url_entry', urlWarning: '.tiny_media_url_warning', loaderIcon: '.tiny_media_loader', loaderIconContainer: '.tiny_media_loader_container', insertMedia: '.tiny_media_insert_media', modalFooter: '.modal-footer', fileNameLabel: '.tiny_media_filename', previewBox: '.tiny_media_preview_box', sizeOriginal: '.tiny_media_sizeoriginal', sizeCustom: '.tiny_media_sizecustom', properties: '.tiny_media_properties', mediaDetailsBody: '#tiny-media-details-body', mediaSubtitleCaptionBody: '#tiny-media-subtitle-caption-body', mediaMetadataTabPane: '.tab-pane-media-metadata', videoThumbnail: '.video-thumbnail', mediaSizeProperties: '.media-size-properties', thumbnailPreview: '.tiny_media_thumbnail_preview', mediaPreviewContainer: '#media-filter-preview-container', }, template: { body: { insertMediaBody: 'tiny_media/embed/body/insert_media_body', mediaDetailsBody: 'tiny_media/embed/body/media_details_body', mediaThumbnailBody: 'tiny_media/embed/body/media_thumbnail_body', }, footer: { insertMediaFooter: 'tiny_media/embed/footer/insert_media_footer', mediaDetailsFooter: 'tiny_media/embed/footer/media_details_footer', mediaThumbnailFooter: 'tiny_media/embed/footer/media_thumbnail_footer', }, }, type: 'EMBED', externalMediaProvider: 'external-media-provider', }, }; PK/<\2amd/src/imagemodal.jsnu[// 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_media/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(); PK/<\J ))amd/src/usedfiles.jsnu[// 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_media/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(userContext, itemId, elementId) { this.files = this.getFiles(); 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_media/missingfiles', { missingFiles, }).then(({html, js}) => { Templates.replaceNodeContents(form.querySelector('.missing-files'), html, js); return; }); } /** * Retrieves a list of existing files selected for deletion. * * @returns {Object} An object where the keys are filenames and the values are file hashes. * */ getFiles() { const files = {}; document.querySelectorAll('input[type=checkbox][name^="deletefile"]').forEach(input => { files[input.dataset.filename] = input.dataset.filehash; }); return files; } } export const init = (files, usercontext, itemid, elementid) => { const manager = new UsedFileManager(files, usercontext, itemid, elementid); manager.updateFiles(); return manager; }; PK/<\絥Zamd/src/helpers.jsnu[// 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 helpers for image and embed. * * @module tiny_media/helpers * @copyright 2024 Stevani Andolo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Templates from 'core/templates'; import Selectors from './selectors'; import Config from 'core/config'; /** * Renders and inserts the body template for inserting an media 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 body = async(templateContext, root) => { return Templates.renderForPromise(templateContext.bodyTemplate, {...templateContext}) .then(({html, js}) => { Templates.replaceNodeContents(root.querySelector(Selectors[templateContext.selector].elements.bodyTemplate), html, js); return; }) .catch(error => { window.console.log(error); }); }; /** * Renders and inserts the footer template for inserting an media 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 footer = async(templateContext, root) => { return Templates.renderForPromise(templateContext.footerTemplate, {...templateContext}) .then(({html, js}) => { Templates.replaceNodeContents(root.querySelector(Selectors[templateContext.selector].elements.footerTemplate), html, js); return; }) .catch(error => { window.console.log(error); }); }; /** * Set extra properties on an instance using incoming data. * * @param {object} instance * @param {object} data * @return {object} Modified instance */ export const setPropertiesFromData = async(instance, data) => { for (const property in data) { if (typeof data[property] !== 'function') { instance[property] = data[property]; } } return instance; }; /** * Check if given string is a valid URL. * * @param {String} urlString URL the link will point to. * @returns {boolean} True is valid, otherwise false. */ export const 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); }; /** * 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'); } } }; /** * 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'); } } }; /** * Displays the upload loader and disables UI elements while loading a file. * * @param {html} root Modal element * @param {string} selector String of type IMAGE/EMBED */ export const startMediaLoading = (root, selector) => { showElements(Selectors[selector].elements.loaderIcon, root); const elementsToHide = [ Selectors[selector].elements.insertMedia, Selectors[selector].elements.urlWarning, Selectors[selector].elements.modalFooter, ]; hideElements(elementsToHide, root); }; /** * Hide the upload loader and enable UI elements when loaded. * * @param {html} root Modal element * @param {string} selector String of type IMAGE/EMBED */ export const stopMediaLoading = (root, selector) => { hideElements(Selectors[selector].elements.loaderIcon, root); const elementsToShow = [ Selectors[selector].elements.insertMedia, Selectors[selector].elements.modalFooter, ]; showElements(elementsToShow, root); }; /** * Return true or false if the url is external. * * @param {string} url * @returns {boolean} True if the URL is external, otherwise false. */ export const isExternalUrl = (url) => { const regex = new RegExp(`${Config.wwwroot}`); // True if the URL is from external, otherwise false. return regex.test(url) === false; }; /** * Set the string for the URL label element. * * @param {object} props - The label text to set. */ export const setFilenameLabel = (props) => { const urlLabelEle = props.root.querySelector(props.fileNameSelector); if (urlLabelEle) { urlLabelEle.innerHTML = props.label; urlLabelEle.setAttribute("title", props.label); } }; /** * 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. * * @param {object} props */ export const sourceTypeChecked = (props) => { if (!isExternalUrl(props.source)) { // Split the URL by '/' to get an array of segments. const segments = props.source.split('/'); // Get the last segment, which should be the filename. const filename = segments.pop().split('?')[0]; // Show the file name. props.label = decodeURI(filename); } else { // For external URLs, always show the full URL in the footer for consistency. props.label = decodeURI(props.source); } setFilenameLabel(props); }; /** * Get filename from the name label. * * @param {string} fileLabel * @returns {string} */ export const getFileName = (fileLabel) => { if (fileLabel.includes('/')) { const split = fileLabel.split('/'); let fileName = split[split.length - 1]; fileName = fileName.split('.'); if (fileName.length > 1) { return decodeURI(fileName.slice(0, (fileName.length - 1)).join('.')); } else { return decodeURI(fileName[0]); } } else { return decodeURI(fileLabel.split('.')[0]); } }; /** * Return true or false if % is found. * * @param {string} value * @returns {boolean} */ export const isPercentageValue = (value) => { return value.match(/\d+%/); }; PK/<\V~~amd/build/common.min.js.mapnu[{"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_media/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_media/plugin',\n component: 'tiny_media',\n imageButtonName: 'tiny_media_image',\n videoButtonName: 'tiny_media_video',\n mediaManagerButtonName: 'tiny_mediamanager',\n};\n"],"names":["pluginName","component","imageButtonName","videoButtonName","mediaManagerButtonName"],"mappings":"mKAuBe,CACXA,WAAY,oBACZC,UAAW,aACXC,gBAAiB,mBACjBC,gBAAiB,mBACjBC,uBAAwB"}PK/<\Üamd/build/usedfiles.min.js.mapnu[{"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_media/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(userContext, itemId, elementId) {\n this.files = this.getFiles();\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_media/missingfiles', {\n missingFiles,\n }).then(({html, js}) => {\n Templates.replaceNodeContents(form.querySelector('.missing-files'), html, js);\n return;\n });\n }\n\n /**\n * Retrieves a list of existing files selected for deletion.\n *\n * @returns {Object} An object where the keys are filenames and the values are file hashes.\n *\n */\n getFiles() {\n const files = {};\n document.querySelectorAll('input[type=checkbox][name^=\"deletefile\"]').forEach(input => {\n files[input.dataset.filename] = input.dataset.filehash;\n });\n return files;\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","userContext","itemId","elementId","files","this","getFiles","getElementId","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","input","filehash","usercontext","itemid","elementid","manager"],"mappings":";;;;;;;+EA0BMA,gBACFC,YAAYC,YAAaC,OAAQC,gBACxBC,MAAQC,KAAKC,gBACbL,YAAcA,iBACdC,OAASA,YACTC,UAAYA,UAGrBI,sBACWF,KAAKF,UAGhBK,qBACUC,OAASC,OAAOC,OAAOC,QAAQC,cAAcC,IAAIT,KAAKE,oBACvDE,cACDC,OAAOK,QAAQC,qCAA8BX,KAAKE,iBAC3C,SAELU,QAAUR,OAAOS,aACjBC,kBAAaC,gBAAOC,kCAAyBhB,KAAKJ,mCAA0BI,KAAKH,YACjFoB,QAAU,IAAIC,OAAO,QAAUJ,QAAQK,QAAQ,wBAAyB,QAAU,2BAA4B,YAElG,IAAIP,QAAQQ,SAASH,UAAUI,KAAKC,OAAUC,mBAAmBD,MAAME,OAAOC,YAMpGC,gBAAgBC,kBACLC,OAAOC,QAAQ7B,KAAKD,OACtB+B,QAAOC,WAAEN,sBAAeE,UAAUK,SAASP,aAC3CJ,KAAIY,YAAER,uBAAcA,YAI7BS,iBAAiBP,kBACNA,UAAUG,QAAQL,WAAczB,KAAKD,MAAMoC,eAAeV,YAGrEW,oBACUC,KAAOC,SAASC,cAAc,QAC9BZ,UAAY3B,KAAKG,eACjBqC,YAAcxC,KAAK0B,gBAAgBC,WACnCc,aAAezC,KAAKkC,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,0BAA2B,CACzDX,aAAAA,eACDY,MAAKC,YAACC,KAACA,KAADC,GAAOA,UACZL,UAAUM,oBAAoBpB,KAAKE,cAAc,kBAAmBgB,KAAMC,OAWjFvD,iBACSF,MAAQ,UACduC,SAASI,iBAAiB,4CAA4CC,SAAQe,QAC1E3D,MAAM2D,MAAMb,QAAQpB,UAAYiC,MAAMb,QAAQc,YAE3C5D,qBAIK,CAACA,MAAO6D,YAAaC,OAAQC,mBACvCC,QAAU,IAAIrE,gBAAgBK,MAAO6D,YAAaC,OAAQC,kBAChEC,QAAQ3B,cAED2B"}PK/<\+<amd/build/imagemodal.min.jsnu[define("tiny_media/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.mapPK/<\"Oך##amd/build/imagemodal.min.js.mapnu[{"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_media/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":"8ZA0BqBA,mBAAmBC,eAIpCC,+BAEUA,8BAGDC,2BACAC,wBAGTC,UAAUC,aACNA,YAAYC,OAAQ,EACpBD,YAAYE,eAAgB,EAC5BF,YAAYG,MAAO,QAEbJ,UAAUC,iEAlBHN,4BACAU,kDADAV,gCAEIU,0CAoBzBV,WAAWW"}PK/<\amd/build/imageinsert.min.jsnu[define("tiny_media/imageinsert",["exports","./selectors","core/dropzone","editor_tiny/uploader","core/prefetch","core/str","./common","editor_tiny/options","editor_tiny/utils","tiny_media/imagedetails","./helpers","./imagehelpers"],(function(_exports,_selectors,_dropzone,_uploader,_prefetch,_str,_common,_options,_utils,_imagedetails,_helpers,_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_media",["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,"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,_helpers.showElements)(_selectors.default.IMAGE.elements.urlWarning,this.root),this.currentUrl="",this.stopImageLoading()})),image.addEventListener("load",(()=>{let templateContext={};templateContext.sizecustomhelpicon={text:this.langStrings.sizecustom_help},templateContext.bodyTemplate=_selectors.default.IMAGE.template.body.insertImageDetailsBody,templateContext.footerTemplate=_selectors.default.IMAGE.template.footer.insertImageDetailsFooter,templateContext.selector=_selectors.default.IMAGE.type,templateContext.maxlengthalt=_imagehelpers.MAX_LENGTH_ALT,Promise.all([(0,_helpers.body)(templateContext,this.root),(0,_helpers.footer)(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,_helpers.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&&(0,_helpers.isValidUrl)(url))}urlChanged(){(0,_helpers.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,_helpers.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,_helpers.hideElements)(elementsToHide,this.root)}stopImageLoading(){(0,_helpers.hideElements)(_selectors.default.IMAGE.elements.loaderIcon,this.root);const elementsToShow=[_selectors.default.IMAGE.elements.insertImage,_selectors.default.IMAGE.elements.modalFooter];(0,_helpers.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.mapPK/<\#rr!amd/build/imagedetails.min.js.mapnu[{"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_media/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_media/imageinsert';\nimport {MediaBase} from './mediabase';\nimport {\n body,\n footer,\n hideElements,\n showElements,\n isPercentageValue,\n} from './helpers';\n\nexport class ImageDetails extends MediaBase {\n DEFAULTS = {\n WIDTH: 160,\n HEIGHT: 160,\n };\n\n selectorType = Selectors.IMAGE.type;\n\n mediaDimensions = null;\n\n constructor(\n root,\n editor,\n currentModal,\n canShowFilePicker,\n canShowDropZone,\n currentUrl,\n image,\n ) {\n super();\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 this.toggleMaxlengthFeedbackSuffix = false;\n }\n\n init = function() {\n this.currentModal.setTitle(getString('imagedetails', 'tiny_media'));\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 bodyTemplate: Selectors.IMAGE.template.body.insertImageBody,\n footerTemplate: Selectors.IMAGE.template.footer.insertImageFooter,\n selector: Selectors.IMAGE.type,\n };\n\n Promise.all([body(templateContext, this.root), footer(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.mediaDimensions = {\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.mediaDimensions.width;\n }\n return element.value;\n };\n\n const getCurrentHeight = (element) => {\n if (element.value === '') {\n element.value = this.mediaDimensions.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.mediaDimensions.width === currentWidth &&\n this.mediaDimensions.height === currentHeight\n ) {\n this.currentWidth = this.mediaDimensions.width;\n this.currentHeight = this.mediaDimensions.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 * 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 changes in the image presentation checkbox and enables/disables the image alt text input accordingly.\n */\n async 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 await 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.altWarning, 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_media: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_media/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_media'),\n getString('deleteimagewarning', 'tiny_media'),\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', async(e) => {\n const presentationEle = e.target.closest(Selectors.IMAGE.elements.presentation);\n if (presentationEle) {\n await 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', async(e) => {\n if (e.target.nodeType === Node.ELEMENT_NODE) {\n\n const presentationEle = e.target.closest(Selectors.IMAGE.elements.presentation);\n if (presentationEle) {\n await this.presentationChanged();\n }\n }\n }, true);\n\n // Character count.\n this.root.addEventListener('keyup', async(e) => {\n const altEle = e.target.closest(Selectors.IMAGE.elements.alt);\n if (altEle) {\n await 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 async handleKeyupCharacterCount() {\n const altField = this.root.querySelector(Selectors.IMAGE.elements.alt);\n const alt = altField.value;\n const current = this.root.querySelector('#currentcount');\n current.innerHTML = alt.length;\n const maxLength = altField.getAttribute('maxlength');\n const maxLengthFeedback = document.getElementById('maxlength_feedback');\n if (alt.length >= maxLength) {\n maxLengthFeedback.textContent = await getString('maxlengthreached', 'core', maxLength);\n\n // Clever (or hacky?;p) way to ensure that the feedback message is announced to screen readers.\n const suffix = this.toggleMaxlengthFeedbackSuffix ? '' : '.';\n maxLengthFeedback.textContent += suffix;\n this.toggleMaxlengthFeedbackSuffix = !this.toggleMaxlengthFeedbackSuffix;\n\n // Clear the feedback message after 4 seconds. This is similar to the default timeout of toast messages\n // before disappearing from view. It is important to clear the message to prevent screen reader users from navigating\n // into this region and avoiding confusion.\n setTimeout(() => {\n maxLengthFeedback.textContent = '';\n }, 4000);\n }\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":["ImageDetails","MediaBase","constructor","root","editor","currentModal","canShowFilePicker","canShowDropZone","currentUrl","image","WIDTH","HEIGHT","Selectors","IMAGE","type","setTitle","imageTypeChecked","presentationChanged","storeImageDimensions","this","setImageDimensions","registerEventListeners","async","templateContext","elementid","id","showfilepicker","showdropzone","bodyTemplate","template","body","insertImageBody","footerTemplate","footer","insertImageFooter","selector","Promise","all","then","ImageInsert","init","catch","error","window","console","log","imagePreviewBox","querySelector","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","toggleMaxlengthFeedbackSuffix","mediaDimensions","DEFAULTS","currentWidth","element","getCurrentWidth","currentHeight","getCurrentHeight","setAttribute","src","display","constrain","checked","disabled","widthRatio","round","parseInt","heightRatio","sizeChecked","setSelectedSize","Number","presentation","alt","handleKeyupCharacterCount","isExternalUrl","RegExp","Config","wwwroot","test","url","setFilenameLabel","decodeURI","filename","split","pop","label","urlLabelEle","fileNameLabel","innerHTML","toggleAriaInvalid","selectors","predicate","forEach","querySelectorAll","hasErrorUrlField","urlError","urlWarning","hasErrorAltField","imageAltError","altWarning","updateWarning","getImageContext","classList","sizeOriginal","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","autoAdjustSize","sizeCustom","nodeType","Node","ELEMENT_NODE","widthEle","heightEle","altField","length","maxLength","getAttribute","maxLengthFeedback","document","getElementById","textContent","suffix","setTimeout"],"mappings":"m7BAwCaA,qBAAqBC,qBAU9BC,YACIC,KACAC,OACAC,aACAC,kBACAC,gBACAC,WACAC,gDAhBO,CACPC,MAAO,IACPC,OAAQ,0CAGGC,mBAAUC,MAAMC,6CAEb,mCAsBX,gBACET,aAAaU,UAAS,kBAAU,eAAgB,oBAChDC,wBACAC,2BACAC,qBAAqBC,KAAKV,YAC1BW,0BACAC,oEAMSC,uBACRC,gBAAkB,CACpBC,UAAWL,KAAKf,OAAOqB,GACvBC,eAAgBP,KAAKb,kBACrBqB,aAAcR,KAAKZ,gBACnBqB,aAAchB,mBAAUC,MAAMgB,SAASC,KAAKC,gBAC5CC,eAAgBpB,mBAAUC,MAAMgB,SAASI,OAAOC,kBAChDC,SAAUvB,mBAAUC,MAAMC,MAG9BsB,QAAQC,IAAI,EAAC,iBAAKd,gBAAiBJ,KAAKhB,OAAO,mBAAOoB,gBAAiBJ,KAAKhB,QACvEmC,MAAK,KACkB,IAAIC,yBACpBpB,KAAKhB,KACLgB,KAAKf,OACLe,KAAKd,aACLc,KAAKb,kBACLa,KAAKZ,iBAEGiC,UAGfC,OAAMC,QACHC,OAAOC,QAAQC,IAAIH,wDA2EV,WACXI,gBAAkB3B,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASC,YACnExC,MAAQU,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASE,SACzDC,WAAahC,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASI,OAC9DC,YAAclC,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASM,QAE/DC,sBAAwB,WAEpBC,SAAWV,gBAAgBW,YAC3BC,UAAYZ,gBAAgBa,aAE5BC,WAAazC,KAAK0C,iBAAiBV,WAAWW,MAAOT,YAAYS,MAAON,SAAUE,WACxFjD,MAAMsD,MAAMX,gBAAWQ,WAAWR,YAClC3C,MAAMsD,MAAMT,iBAAYM,WAAWN,cAGH,IAAhCR,gBAAgBW,iBAEXpD,aAAa2D,UAAUC,GAAGC,sBAAYC,OAAO,KAC9CZ,2BAGJA,oEAkSW,CAACa,YAAaC,aAAcb,SAAUE,gBACjDU,YAAcZ,UAAYa,aAAeX,gBAEpC,CACLN,MAAOgB,YACPd,OAAQe,oBAINC,iBAAmBd,SAAWY,YAC9BG,kBAAoBb,UAAYW,aAChCG,eAAiBC,KAAKC,IAAIJ,iBAAkBC,yBAI3C,CACLnB,MAHegB,YAAcI,eAI7BlB,OAHgBe,aAAeG,wBA9b5BrE,KAAOA,UACPC,OAASA,YACTC,aAAeA,kBACfC,kBAAoBA,uBACpBC,gBAAkBA,qBAClBC,WAAaA,gBACbC,MAAQA,YACRkE,+BAAgC,EA0CzCzD,qBAAqBT,YAEZmE,gBAAkB,CACnBxB,MAAO3C,MAAM2C,OAASjC,KAAK0D,SAASnE,MACpC4C,OAAQ7C,MAAM6C,QAAUnC,KAAK0D,SAASlE,cAkBpCmE,aAfmBC,CAAAA,UACC,KAAlBA,QAAQjB,QACRiB,QAAQjB,MAAQ3C,KAAKyD,gBAAgBxB,OAElC2B,QAAQjB,OAWEkB,CADF7D,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASI,QAI9D6B,cAXoBF,CAAAA,UACA,KAAlBA,QAAQjB,QACRiB,QAAQjB,MAAQ3C,KAAKyD,gBAAgBtB,QAElCyB,QAAQjB,OAOGoB,CADF/D,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASM,SAG/DJ,QAAU/B,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASE,SACjEA,QAAQiC,aAAa,MAAO1E,MAAM2E,KAClClC,QAAQa,MAAMsB,QAAU,SAGlBC,UAAYnE,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASsC,eAC/D,8BAAkBR,gBAAiB,8BAAkBG,eACrDK,UAAUC,QAAUT,eAAiBG,mBAClC,GAAoB,IAAhBxE,MAAM2C,OAAgC,IAAjB3C,MAAM6C,OAElCgC,UAAUE,SAAW,eAClB,OAEGC,WAAahB,KAAKiB,MAAM,IAAMC,SAASb,aAAc,IAAMrE,MAAM2C,OACjEwC,YAAcnB,KAAKiB,MAAM,IAAMC,SAASV,cAAe,IAAMxE,MAAM6C,QACzEgC,UAAUC,QAAUE,aAAeG,YASf,EAACd,aAAcG,iBAC/B9D,KAAKyD,gBAAgBxB,QAAU0B,cAC/B3D,KAAKyD,gBAAgBtB,SAAW2B,oBAE3BH,aAAe3D,KAAKyD,gBAAgBxB,WACpC6B,cAAgB9D,KAAKyD,gBAAgBtB,YACrCuC,YAAY,mBAEZf,aAAeA,kBACfG,cAAgBA,mBAChBY,YAAY,YAIzBC,CAAgBC,OAAOjB,cAAeiB,OAAOd,kDAoCvCe,aAAe7E,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASgD,cAC1D7E,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASiD,KACzDT,SAAWQ,aAAaT,cAGtBpE,KAAK+E,4BASflF,yBAIUmF,eAAgD,IAHxC,IAAIC,iBAAUC,gBAAOC,UAGPC,KAAKpF,KAAKX,yCAGzBI,mBAAUC,MAAMmC,SAASwD,IAAKrF,KAAKhB,MAE3CgG,mBASIM,iBAAiBC,UAAUvF,KAAKX,iBATrB,OAIVmG,SAFWxF,KAAKX,WAAWoG,MAAM,KAEbC,MAAMD,MAAM,KAAK,QAEtCH,iBAAiBC,UAAUC,YAYxCF,iBAAiBK,aACPC,YAAc5F,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASgE,eACjED,cACAA,YAAYE,UAAYH,MACxBC,YAAY5B,aAAa,QAAS2B,QAI1CI,kBAAkBC,UAAWC,WACzBD,UAAUE,SAASlF,WACEhB,KAAKhB,KAAKmH,iBAAiBnF,UACnCkF,SAAStC,SAAYA,QAAQI,aAAa,eAAgBiC,gBAI3EG,yBACUC,SAA+B,KAApBrG,KAAKX,kBAClBgH,mCACa5G,mBAAUC,MAAMmC,SAASyE,WAAYtG,KAAKhB,gCAE1CS,mBAAUC,MAAMmC,SAASyE,WAAYtG,KAAKhB,WAEtD+G,kBAAkB,CAACtG,mBAAUC,MAAMmC,SAASwD,KAAMgB,UAEhDA,SAGXE,yBACUzB,IAAM9E,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASiD,KAAKnC,MAC5DkC,aAAe7E,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASgD,cAAcT,QAC9EoC,cAAwB,KAAR1B,MAAeD,oBACjC2B,wCACa/G,mBAAUC,MAAMmC,SAAS4E,WAAYzG,KAAKhB,gCAE1CS,mBAAUC,MAAMmC,SAAS4E,WAAYzG,KAAKhB,WAEtD+G,kBAAkB,CAACtG,mBAAUC,MAAMmC,SAASiD,IAAKrF,mBAAUC,MAAMmC,SAASgD,cAAe2B,eAEvFA,cAGXE,sBACUL,SAAWrG,KAAKoG,mBAChBI,cAAgBxG,KAAKuG,0BAEpBF,UAAYG,cAGvBG,qBAEQ3G,KAAK0G,uBACE,WAGLE,UAAY,GACZzC,UAAYnE,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASsC,WAAWC,QACxEyC,aAAe7G,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASgF,cAAczC,eAChFD,WAAa0C,aAEbD,UAAUE,KAAKrH,mBAAUC,MAAMqH,OAAOC,YAGtCJ,UAAUlB,IAAIjG,mBAAUC,MAAMqH,OAAOC,YAGlC,CACH3B,IAAKrF,KAAKX,WACVyF,IAAK9E,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASiD,KAAKnC,MAC3DV,MAAOjC,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASI,OAAOU,MAC/DR,OAAQnC,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASM,QAAQQ,MACjEkC,aAAc7E,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASgD,cAAcT,QAC7E6C,YAAajH,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASoF,aAAatE,MAC3EuE,UAAWN,UAAUO,KAAK,MAIlCC,iBACUC,eAAiB,IAAIC,iBAAQ,0BAEvB,KADAtH,KAAKX,qBAMbW,KAAK0G,4BACLW,eAAeE,gBAKbtF,MAAQjC,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASI,OAAOU,WACjE,8BAAkBV,QAAUuF,MAAMhD,SAASvC,MAAO,iBAC9CjD,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASI,OAAOwF,aACxDJ,eAAeE,gBAIbpF,OAASnC,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASM,QAAQQ,WACnE,8BAAkBR,SAAWqF,MAAMhD,SAASrC,OAAQ,iBAChDnD,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASM,QAAQsF,aACzDJ,eAAeE,6BAITG,OAAO,mBAAoB1H,KAAK2G,mBACzCxF,MAAMwG,YACE1I,OAAO2I,cAAcD,WACrBzI,aAAa2I,UAClBR,eAAeE,UAERI,QAEVrG,OAAMC,QACHC,OAAOC,QAAQC,IAAIH,UAO3BuG,oCACiBC,qBACT,kBAAU,cAAe,eACzB,kBAAU,qBAAsB,eAClC5G,MAAK,+BACU1B,mBAAUC,MAAMmC,SAAS4E,WAAYzG,KAAKhB,WAElDgJ,qBAEN1G,OAAMC,QACLC,OAAOC,QAAQC,IAAIH,UAI3BrB,yBACyBF,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMuI,QAAQC,QACxDC,iBAAiB,SAAUC,IACpCA,EAAEC,sBACGjB,oBAGHkB,eAAiBtI,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMuI,QAAQH,aACvEQ,eAAeH,iBAAiB,SAAS,UAChCL,iBAETQ,eAAeH,iBAAiB,WAAYC,IAC1B,UAAVA,EAAEG,UACGT,sBAIR9I,KAAKmJ,iBAAiB,UAAUhI,MAAAA,IACTiI,EAAEI,OAAOC,QAAQhJ,mBAAUC,MAAMmC,SAASgD,qBAExD7E,KAAKF,sBAGMsI,EAAEI,OAAOC,QAAQhJ,mBAAUC,MAAMmC,SAASsC,iBAEtDuE,iBAGeN,EAAEI,OAAOC,QAAQhJ,mBAAUC,MAAMmC,SAASgF,oBAEzDnC,YAAY,YAGC0D,EAAEI,OAAOC,QAAQhJ,mBAAUC,MAAMmC,SAAS8G,kBAEvDjE,YAAY,kBAIpB1F,KAAKmJ,iBAAiB,QAAQhI,MAAAA,OAC3BiI,EAAEI,OAAOI,WAAaC,KAAKC,aAAc,CAEjBV,EAAEI,OAAOC,QAAQhJ,mBAAUC,MAAMmC,SAASgD,qBAExD7E,KAAKF,0BAGpB,QAGEd,KAAKmJ,iBAAiB,SAAShI,MAAAA,IACjBiI,EAAEI,OAAOC,QAAQhJ,mBAAUC,MAAMmC,SAASiD,YAE/C9E,KAAK+E,oCAId/F,KAAKmJ,iBAAiB,SAAUC,UAC3BW,SAAWX,EAAEI,OAAOC,QAAQhJ,mBAAUC,MAAMmC,SAASI,OACvD8G,WAEAA,SAASpG,MAA2B,KAAnBoG,SAASpG,MAAe,EAAIiC,OAAOmE,SAASpG,YACxD+F,wBAGHM,UAAYZ,EAAEI,OAAOC,QAAQhJ,mBAAUC,MAAMmC,SAASM,QACxD6G,YAEAA,UAAUrG,MAA4B,KAApBqG,UAAUrG,MAAe,EAAIiC,OAAOoE,UAAUrG,YAC3D+F,gBAAe,+CAMtBO,SAAWjJ,KAAKhB,KAAK4C,cAAcnC,mBAAUC,MAAMmC,SAASiD,KAC5DA,IAAMmE,SAAStG,MACL3C,KAAKhB,KAAK4C,cAAc,iBAChCkE,UAAYhB,IAAIoE,aAClBC,UAAYF,SAASG,aAAa,aAClCC,kBAAoBC,SAASC,eAAe,yBAC9CzE,IAAIoE,QAAUC,UAAW,CACzBE,kBAAkBG,kBAAoB,kBAAU,mBAAoB,OAAQL,iBAGtEM,OAASzJ,KAAKwD,8BAAgC,GAAK,IACzD6F,kBAAkBG,aAAeC,YAC5BjG,+BAAiCxD,KAAKwD,8BAK3CkG,YAAW,KACPL,kBAAkBG,YAAc,KACjC"}PK/<\[A_77amd/build/configuration.min.jsnu[define("tiny_media/configuration",["exports","./common","editor_tiny/uploader","editor_tiny/utils"],(function(_exports,_common,_uploader,_utils){var obj; /** * Tiny Media configuration. * * @module tiny_media/configuration * @copyright 2022 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.configure=void 0,_uploader=(obj=_uploader)&&obj.__esModule?obj:{default:obj};_exports.configure=instanceConfig=>{return{contextmenu:(0,_utils.addContextmenuItem)(instanceConfig.contextmenu,_common.imageButtonName,_common.videoButtonName),menu:(menu=instanceConfig.menu,menu.insert.items.match(/\bmedia\b/)?menu.insert.items=menu.insert.items.replace(/\bmedia\b/,_common.videoButtonName):menu.insert.items="".concat(_common.videoButtonName," ").concat(menu.insert.items),menu.insert.items.match(/\bimage\b/)?menu.insert.items=menu.insert.items.replace(/\bimage\b/,_common.imageButtonName):menu.insert.items="".concat(_common.imageButtonName," ").concat(menu.insert.items),menu.tools.items+=" ".concat(_common.mediaManagerButtonName),menu),toolbar:(toolbar=instanceConfig.toolbar,toolbar.map((section=>("content"===section.name&§ion.items.unshift(_common.imageButtonName,_common.videoButtonName),section)))),images_upload_handler:(blobInfo,progress)=>(0,_uploader.default)(window.tinymce.activeEditor,"image",blobInfo.blob(),blobInfo.filename(),progress),images_reuse_filename:!0};var toolbar,menu}})); //# sourceMappingURL=configuration.min.js.mapPK/<\*55amd/build/image.min.js.mapnu[{"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_media/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_media/imageinsert';\nimport {ImageDetails} from 'tiny_media/imagedetails';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getString} from 'core/str';\nimport {\n body,\n footer,\n hideElements,\n showElements,\n isPercentageValue,\n} from './helpers';\nimport {MAX_LENGTH_ALT} from './imagehelpers';\n\nprefetchStrings('tiny_media', [\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 bodyTemplate: Selectors.IMAGE.template.body.insertImageBody,\n footerTemplate: Selectors.IMAGE.template.footer.insertImageFooter,\n selector: Selectors.IMAGE.type,\n };\n\n Promise.all([body(templateContext, this.root), footer(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_media');\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_media')};\n templateContext.bodyTemplate = Selectors.IMAGE.template.body.insertImageDetailsBody;\n templateContext.footerTemplate = Selectors.IMAGE.template.footer.insertImageDetailsFooter;\n templateContext.selector = Selectors.IMAGE.type;\n templateContext.maxlengthalt = MAX_LENGTH_ALT;\n\n Promise.all([body(templateContext, this.root), footer(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","bodyTemplate","Selectors","IMAGE","template","body","insertImageBody","footerTemplate","footer","insertImageFooter","selector","type","Promise","all","root","then","ImageInsert","currentModal","init","catch","error","window","console","log","url","startImageLoading","image","Image","src","addEventListener","querySelector","elements","urlWarning","innerHTML","stopImageLoading","currentImageData","getCurrentImageData","getTemplateContext","sizecustomhelpicon","text","insertImageDetailsBody","insertImageDetailsFooter","maxlengthalt","MAX_LENGTH_ALT","ImageDetails","permissions","options","filepicker","Object","keys","repositories","length","values","some","repository","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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"}PK/<\X ȳ??amd/build/imagehelpers.min.jsnu[define("tiny_media/imagehelpers",["exports","tiny_media/helpers","./selectors"],(function(_exports,Helpers,_selectors){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.showElements=_exports.isPercentageValue=_exports.hideElements=_exports.footerImageInsert=_exports.footerImageDetails=_exports.bodyImageInsert=_exports.bodyImageDetails=_exports.MAX_LENGTH_ALT=void 0,Helpers=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 plugin image helpers. * * @module tiny_media/imagehelpers * @copyright 2024 Meirza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */(Helpers);_exports.MAX_LENGTH_ALT=750;_exports.bodyImageInsert=async(templateContext,root)=>(window.console.warn("This function is deprecated. Please use core_editor/tiny/plugins/media/helpers.body instead."),templateContext.bodyTemplate="tiny_media/insert_image_modal_insert",templateContext.selector=_selectors.Selectors.IMAGE.type,Helpers.body(templateContext,root));_exports.footerImageInsert=async(templateContext,root)=>(window.console.warn("This function is deprecated.\n Please use core_editor/tiny/plugins/media/helpers.footer instead."),templateContext.footerTemplate="tiny_media/insert_image_modal_insert_footer",templateContext.selector=_selectors.Selectors.IMAGE.type,Helpers.footer(templateContext,root));_exports.bodyImageDetails=async(templateContext,root)=>(window.console.warn("This function is deprecated.\n Please use core_editor/tiny/plugins/media/helpers.body instead."),templateContext.bodyTemplate="tiny_media/insert_image_modal_details",templateContext.selector=_selectors.Selectors.IMAGE.type,Helpers.body(templateContext,root));_exports.footerImageDetails=async(templateContext,root)=>(window.console.warn("This function is deprecated.\n Please use core_editor/tiny/plugins/media/helpers.footer instead."),templateContext.footerTemplate="tiny_media/insert_image_modal_details_footer",templateContext.selector=_selectors.Selectors.IMAGE.type,Helpers.footer(templateContext,root));_exports.showElements=(elements,root)=>{window.console.warn("This function is deprecated.\n Please use core_editor/tiny/plugins/media/helpers.showElements instead."),Helpers.showElements(elements,root)};_exports.hideElements=(elements,root)=>{window.console.warn("This function is deprecated.\n Please use core_editor/tiny/plugins/media/helpers.hideElements instead."),Helpers.hideElements(elements,root)};_exports.isPercentageValue=value=>(window.console.warn("This function is deprecated.\n Please use core_editor/tiny/plugins/media/helpers.isPercentageValue instead."),Helpers.isPercentageValue(value))})); //# sourceMappingURL=imagehelpers.min.js.mapPK/<\kamd/build/options.min.jsnu[define("tiny_media/options",["exports","editor_tiny/options","./common"],(function(_exports,_options,_common){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.register=_exports.getPermissions=_exports.getImagePermissions=_exports.getEmbedPermissions=_exports.getData=void 0; /** * Options helper for Tiny Media plugin. * * @module tiny_media/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");_exports.register=editor=>{const registerOption=editor.options.register;registerOption(permissionsName,{processor:"object",default:{image:{filepicker:!1}}}),registerOption(dataName,{processor:"object"})};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)})); //# sourceMappingURL=options.min.js.mapPK/<\JPcamd/build/image.min.jsnu[define("tiny_media/image",["exports","./selectors","./imagemodal","./options","editor_tiny/options","tiny_media/imageinsert","tiny_media/imagedetails","core/prefetch","core/str","./helpers","./imagehelpers"],(function(_exports,_selectors,_imagemodal,_options,_options2,_imageinsert,_imagedetails,_prefetch,_str,_helpers,_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_media",["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,bodyTemplate:_selectors.default.IMAGE.template.body.insertImageBody,footerTemplate:_selectors.default.IMAGE.template.footer.insertImageFooter,selector:_selectors.default.IMAGE.type};Promise.all([(0,_helpers.body)(templateContext,this.root),(0,_helpers.footer)(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_media"),(0,_helpers.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_media")},templateContext.bodyTemplate=_selectors.default.IMAGE.template.body.insertImageDetailsBody,templateContext.footerTemplate=_selectors.default.IMAGE.template.footer.insertImageDetailsFooter,templateContext.selector=_selectors.default.IMAGE.type,templateContext.maxlengthalt=_imagehelpers.MAX_LENGTH_ALT,Promise.all([(0,_helpers.body)(templateContext,this.root),(0,_helpers.footer)(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,_helpers.isPercentageValue)(String(image.width))?image.width:parseInt(image.width,10))(image);0!==width&&(properties.width=width);const height=(image=>(0,_helpers.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,_helpers.showElements)(_selectors.default.IMAGE.elements.loaderIcon,this.root),(0,_helpers.hideElements)(_selectors.default.IMAGE.elements.insertImage,this.root)}stopImageLoading(){(0,_helpers.hideElements)(_selectors.default.IMAGE.elements.loaderIcon,this.root),(0,_helpers.showElements)(_selectors.default.IMAGE.elements.insertImage,this.root)}},_exports.default})); //# sourceMappingURL=image.min.js.mapPK/<\mI!!amd/build/commands.min.js.mapnu[{"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_media/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 imageButtonName,\n videoButtonName,\n mediaManagerButtonName\n} from './common';\nimport MediaImage from './image';\nimport MediaEmbed from './embed';\nimport MediaManager from './manager';\nimport {getButtonImage} from 'editor_tiny/utils';\nimport Selectors from './selectors';\n\nconst isImage = (node) => node.nodeName.toLowerCase() === 'img';\nconst isVideo = (node) => node.nodeName.toLowerCase() === 'video' || node.nodeName.toLowerCase() === 'audio';\n\nconst registerImageCommand = (editor, imageButtonText) => {\n const imageIcon = 'image';\n const handleImageAction = () => {\n const mediaImage = new MediaImage(editor);\n mediaImage.displayDialogue();\n };\n\n // Register the Menu Button as a toggle.\n // This means that when highlighted over an existing Media Image element it will show as toggled on.\n editor.ui.registry.addToggleButton(imageButtonName, {\n icon: imageIcon,\n tooltip: imageButtonText,\n onAction: handleImageAction,\n onSetup: api => {\n return editor.selection.selectorChangedWithUnbind(\n 'img:not([data-mce-object]):not([data-mce-placeholder]),figure.image',\n api.setActive\n ).unbind;\n }\n });\n\n editor.ui.registry.addMenuItem(imageButtonName, {\n icon: imageIcon,\n text: imageButtonText,\n onAction: handleImageAction,\n });\n\n editor.ui.registry.addContextToolbar(imageButtonName, {\n predicate: isImage,\n items: imageButtonName,\n position: 'node',\n scope: 'node'\n });\n\n editor.ui.registry.addContextMenu(imageButtonName, {\n update: isImage,\n });\n\n // Let's check for image file at dragged and dropped and add img-fluid class if it does't have it yet.\n editor.on('SetContent', function() {\n const imgs = editor.getBody().querySelectorAll(`img:not(.${Selectors.IMAGE.styles.responsive})`);\n imgs.forEach(img => img.classList.add(`${Selectors.IMAGE.styles.responsive}`));\n });\n};\n\nconst registerEmbedCommand = (editor, videoButtonText) => {\n const videoIcon = 'embed';\n const handleEmbedAction = () => {\n const mediaImage = new MediaEmbed(editor);\n mediaImage.displayDialogue();\n };\n\n // Register the Menu Button as a toggle.\n // This means that when highlighted over an existing Media Video element it will show as toggled on.\n editor.ui.registry.addToggleButton(videoButtonName, {\n icon: videoIcon,\n tooltip: videoButtonText,\n onAction: handleEmbedAction,\n onSetup: api => {\n return editor.selection.selectorChangedWithUnbind(\n 'video:not([data-mce-object]):not([data-mce-placeholder]),' +\n 'audio:not([data-mce-object]):not([data-mce-placeholder]),' +\n 'a:not([data-mce-object]):not([data-mce-placeholder])[class=external-media-provider]',\n api.setActive\n ).unbind;\n }\n });\n\n editor.ui.registry.addMenuItem(videoButtonName, {\n icon: videoIcon,\n text: videoButtonText,\n onAction: handleEmbedAction,\n });\n\n editor.ui.registry.addContextMenu(videoButtonName, {\n update: isVideo,\n });\n\n editor.ui.registry.addContextToolbar(videoButtonName, {\n predicate: isVideo,\n items: videoButtonName,\n position: 'node',\n scope: 'node'\n });\n\n};\n\nconst registerManagerCommand = (editor, mediaManagerButtonText, mediaManagerButtonImage) => {\n const mediaManagerIcon = 'filemanager';\n const handleMediaManager = () => {\n const mediaManager = new MediaManager(editor);\n mediaManager.displayDialogue();\n };\n\n // Register the Menu Button as a toggle.\n editor.ui.registry.addIcon(mediaManagerIcon, mediaManagerButtonImage.html);\n editor.ui.registry.addButton(mediaManagerButtonName, {\n icon: mediaManagerIcon,\n tooltip: mediaManagerButtonText,\n onAction: () => {\n handleMediaManager();\n }\n });\n\n editor.ui.registry.addMenuItem(mediaManagerButtonName, {\n icon: mediaManagerIcon,\n text: mediaManagerButtonText,\n onAction: () => {\n handleMediaManager();\n }\n });\n};\n\nexport const getSetup = async() => {\n const [\n imageButtonText,\n mediaButtonText,\n mediaManagerButtonText\n ] = await getStrings(['imagebuttontitle', 'mediabuttontitle', 'mediamanagerbuttontitle'].map((key) => ({key, component})));\n\n const [\n mediaManagerButtonImage,\n ] = await Promise.all([\n getButtonImage('filemanager', 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 registerImageCommand(editor, imageButtonText);\n registerEmbedCommand(editor, mediaButtonText);\n registerManagerCommand(editor, mediaManagerButtonText, mediaManagerButtonImage);\n };\n};\n"],"names":["isImage","node","nodeName","toLowerCase","isVideo","async","imageButtonText","mediaButtonText","mediaManagerButtonText","map","key","component","mediaManagerButtonImage","Promise","all","editor","handleImageAction","MediaImage","displayDialogue","ui","registry","addToggleButton","imageButtonName","icon","tooltip","onAction","onSetup","api","selection","selectorChangedWithUnbind","setActive","unbind","addMenuItem","text","addContextToolbar","predicate","items","position","scope","addContextMenu","update","on","getBody","querySelectorAll","Selectors","IMAGE","styles","responsive","forEach","img","classList","add","registerImageCommand","videoButtonText","handleEmbedAction","MediaEmbed","videoButtonName","registerEmbedCommand","handleMediaManager","MediaManager","addIcon","html","addButton","mediaManagerButtonName","registerManagerCommand"],"mappings":";;;;;;;gQAoCMA,QAAWC,MAAyC,QAAhCA,KAAKC,SAASC,cAClCC,QAAWH,MAAyC,UAAhCA,KAAKC,SAASC,eAA6D,UAAhCF,KAAKC,SAASC,gCAmH3DE,gBAEhBC,gBACAC,gBACAC,8BACM,mBAAW,CAAC,mBAAoB,mBAAoB,2BAA2BC,KAAKC,OAAUA,IAAAA,IAAKC,UAAAA,wBAGzGC,+BACMC,QAAQC,IAAI,EAClB,yBAAe,cAAeH,4BAK1BI,SAhIiB,EAACA,OAAQT,yBAE5BU,kBAAoB,KACH,IAAIC,eAAWF,QACvBG,mBAKfH,OAAOI,GAAGC,SAASC,gBAAgBC,wBAAiB,CAChDC,KATc,QAUdC,QAASlB,gBACTmB,SAAUT,kBACVU,QAASC,KACEZ,OAAOa,UAAUC,0BACpB,sEACAF,IAAIG,WACNC,SAIVhB,OAAOI,GAAGC,SAASY,YAAYV,wBAAiB,CAC5CC,KArBc,QAsBdU,KAAM3B,gBACNmB,SAAUT,oBAGdD,OAAOI,GAAGC,SAASc,kBAAkBZ,wBAAiB,CAClDa,UAAWnC,QACXoC,MAAOd,wBACPe,SAAU,OACVC,MAAO,SAGXvB,OAAOI,GAAGC,SAASmB,eAAejB,wBAAiB,CAC/CkB,OAAQxC,UAIZe,OAAO0B,GAAG,cAAc,WACP1B,OAAO2B,UAAUC,oCAA6BC,mBAAUC,MAAMC,OAAOC,iBAC7EC,SAAQC,KAAOA,IAAIC,UAAUC,cAAOP,mBAAUC,MAAMC,OAAOC,mBAwFhEK,CAAqBrC,OAAQT,iBApFR,EAACS,OAAQsC,yBAE5BC,kBAAoB,KACH,IAAIC,eAAWxC,QACvBG,mBAKfH,OAAOI,GAAGC,SAASC,gBAAgBmC,wBAAiB,CAChDjC,KATc,QAUdC,QAAS6B,gBACT5B,SAAU6B,kBACV5B,QAASC,KACEZ,OAAOa,UAAUC,0BACpB,wMAGAF,IAAIG,WACNC,SAIVhB,OAAOI,GAAGC,SAASY,YAAYwB,wBAAiB,CAC5CjC,KAvBc,QAwBdU,KAAMoB,gBACN5B,SAAU6B,oBAGdvC,OAAOI,GAAGC,SAASmB,eAAeiB,wBAAiB,CAC/ChB,OAAQpC,UAGZW,OAAOI,GAAGC,SAASc,kBAAkBsB,wBAAiB,CAClDrB,UAAW/B,QACXgC,MAAOoB,wBACPnB,SAAU,OACVC,MAAO,UAgDPmB,CAAqB1C,OAAQR,iBA3CN,EAACQ,OAAQP,uBAAwBI,iCAEtD8C,mBAAqB,KACF,IAAIC,iBAAa5C,QACzBG,mBAIjBH,OAAOI,GAAGC,SAASwC,QAPM,cAOoBhD,wBAAwBiD,MACrE9C,OAAOI,GAAGC,SAAS0C,UAAUC,+BAAwB,CACjDxC,KATqB,cAUrBC,QAAShB,uBACTiB,SAAU,KACNiC,wBAIR3C,OAAOI,GAAGC,SAASY,YAAY+B,+BAAwB,CACnDxC,KAjBqB,cAkBrBU,KAAMzB,uBACNiB,SAAU,KACNiC,yBAuBJM,CAAuBjD,OAAQP,uBAAwBI"}PK/<\!Camd/build/common.min.jsnu[define("tiny_media/common",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={pluginName:"tiny_media/plugin",component:"tiny_media",imageButtonName:"tiny_media_image",videoButtonName:"tiny_media_video",mediaManagerButtonName:"tiny_mediamanager"},_exports.default})); //# sourceMappingURL=common.min.js.mapPK/<\b1M+M+amd/build/helpers.min.js.mapnu[{"version":3,"file":"helpers.min.js","sources":["../src/helpers.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 helpers for image and embed.\n *\n * @module tiny_media/helpers\n * @copyright 2024 Stevani Andolo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport Selectors from './selectors';\nimport Config from 'core/config';\n\n/**\n * Renders and inserts the body template for inserting an media 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 body = async(templateContext, root) => {\n return Templates.renderForPromise(templateContext.bodyTemplate, {...templateContext})\n .then(({html, js}) => {\n Templates.replaceNodeContents(root.querySelector(Selectors[templateContext.selector].elements.bodyTemplate), 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 media 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 footer = async(templateContext, root) => {\n return Templates.renderForPromise(templateContext.footerTemplate, {...templateContext})\n .then(({html, js}) => {\n Templates.replaceNodeContents(root.querySelector(Selectors[templateContext.selector].elements.footerTemplate), html, js);\n return;\n })\n .catch(error => {\n window.console.log(error);\n });\n};\n\n/**\n * Set extra properties on an instance using incoming data.\n *\n * @param {object} instance\n * @param {object} data\n * @return {object} Modified instance\n */\nexport const setPropertiesFromData = async(instance, data) => {\n for (const property in data) {\n if (typeof data[property] !== 'function') {\n instance[property] = data[property];\n }\n }\n return instance;\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 */\nexport const 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 * 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 * 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 * Displays the upload loader and disables UI elements while loading a file.\n *\n * @param {html} root Modal element\n * @param {string} selector String of type IMAGE/EMBED\n */\nexport const startMediaLoading = (root, selector) => {\n showElements(Selectors[selector].elements.loaderIcon, root);\n const elementsToHide = [\n Selectors[selector].elements.insertMedia,\n Selectors[selector].elements.urlWarning,\n Selectors[selector].elements.modalFooter,\n ];\n hideElements(elementsToHide, root);\n};\n\n/**\n * Hide the upload loader and enable UI elements when loaded.\n *\n * @param {html} root Modal element\n * @param {string} selector String of type IMAGE/EMBED\n */\nexport const stopMediaLoading = (root, selector) => {\n hideElements(Selectors[selector].elements.loaderIcon, root);\n const elementsToShow = [\n Selectors[selector].elements.insertMedia,\n Selectors[selector].elements.modalFooter,\n ];\n showElements(elementsToShow, root);\n};\n\n/**\n * Return true or false if the url is external.\n *\n * @param {string} url\n * @returns {boolean} True if the URL is external, otherwise false.\n */\nexport const isExternalUrl = (url) => {\n const regex = new RegExp(`${Config.wwwroot}`);\n\n // True if the URL is from external, otherwise false.\n return regex.test(url) === false;\n};\n\n/**\n * Set the string for the URL label element.\n *\n * @param {object} props - The label text to set.\n */\nexport const setFilenameLabel = (props) => {\n const urlLabelEle = props.root.querySelector(props.fileNameSelector);\n if (urlLabelEle) {\n urlLabelEle.innerHTML = props.label;\n urlLabelEle.setAttribute(\"title\", props.label);\n }\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 * @param {object} props\n */\nexport const sourceTypeChecked = (props) => {\n if (!isExternalUrl(props.source)) {\n // Split the URL by '/' to get an array of segments.\n const segments = props.source.split('/');\n // Get the last segment, which should be the filename.\n const filename = segments.pop().split('?')[0];\n // Show the file name.\n props.label = decodeURI(filename);\n } else {\n // For external URLs, always show the full URL in the footer for consistency.\n props.label = decodeURI(props.source);\n }\n setFilenameLabel(props);\n};\n\n/**\n * Get filename from the name label.\n *\n * @param {string} fileLabel\n * @returns {string}\n */\nexport const getFileName = (fileLabel) => {\n if (fileLabel.includes('/')) {\n const split = fileLabel.split('/');\n let fileName = split[split.length - 1];\n fileName = fileName.split('.');\n if (fileName.length > 1) {\n return decodeURI(fileName.slice(0, (fileName.length - 1)).join('.'));\n } else {\n return decodeURI(fileName[0]);\n }\n } else {\n return decodeURI(fileLabel.split('.')[0]);\n }\n};\n\n/**\n * Return true or false if % is found.\n *\n * @param {string} value\n * @returns {boolean}\n */\nexport const isPercentageValue = (value) => {\n return value.match(/\\d+%/);\n};\n"],"names":["async","templateContext","root","Templates","renderForPromise","bodyTemplate","then","_ref","html","js","replaceNodeContents","querySelector","Selectors","selector","elements","catch","error","window","console","log","footerTemplate","_ref2","instance","data","property","urlString","RegExp","test","hideElements","Array","forEach","elementSelector","element","classList","add","showElements","remove","loaderIcon","elementsToHide","insertMedia","urlWarning","modalFooter","elementsToShow","isExternalUrl","url","Config","wwwroot","setFilenameLabel","props","urlLabelEle","fileNameSelector","innerHTML","label","setAttribute","source","decodeURI","filename","split","pop","fileLabel","includes","fileName","length","slice","join","value","match"],"mappings":";;;;;;;ogBAkCoBA,MAAMC,gBAAiBC,OAChCC,mBAAUC,iBAAiBH,gBAAgBI,aAAc,IAAIJ,kBACnEK,MAAKC,WAACC,KAACA,KAADC,GAAOA,4BACAC,oBAAoBR,KAAKS,cAAcC,mBAAUX,gBAAgBY,UAAUC,SAAST,cAAeG,KAAMC,OAGtHM,OAAMC,QACHC,OAAOC,QAAQC,IAAIH,0BAWLhB,MAAMC,gBAAiBC,OAClCC,mBAAUC,iBAAiBH,gBAAgBmB,eAAgB,IAAInB,kBACrEK,MAAKe,YAACb,KAACA,KAADC,GAAOA,6BACAC,oBAAoBR,KAAKS,cAAcC,mBAAUX,gBAAgBY,UAAUC,SAASM,gBAAiBZ,KAAMC,OAGxHM,OAAMC,QACHC,OAAOC,QAAQC,IAAIH,yCAWUhB,MAAMsB,SAAUC,YAC5C,MAAMC,YAAYD,KACW,mBAAnBA,KAAKC,YACZF,SAASE,UAAYD,KAAKC,kBAG3BF,8BASeG,aACH,IAAIC,OAAO,yIAIVC,KAAKF,iBAShBG,aAAe,CAACd,SAAUZ,WAC/BY,oBAAoBe,MACpBf,SAASgB,SAASC,wBACRC,QAAU9B,KAAKS,cAAcoB,iBAC/BC,SACAA,QAAQC,UAAUC,IAAI,iBAG3B,OACGF,QAAU9B,KAAKS,cAAcG,UAC/BkB,SACAA,QAAQC,UAAUC,IAAI,qDAWrBC,aAAe,CAACrB,SAAUZ,WAC/BY,oBAAoBe,MACpBf,SAASgB,SAASC,wBACRC,QAAU9B,KAAKS,cAAcoB,iBAC/BC,SACAA,QAAQC,UAAUG,OAAO,iBAG9B,OACGJ,QAAU9B,KAAKS,cAAcG,UAC/BkB,SACAA,QAAQC,UAAUG,OAAO,0EAWJ,CAAClC,KAAMW,YACpCsB,aAAavB,mBAAUC,UAAUC,SAASuB,WAAYnC,YAChDoC,eAAiB,CACnB1B,mBAAUC,UAAUC,SAASyB,YAC7B3B,mBAAUC,UAAUC,SAAS0B,WAC7B5B,mBAAUC,UAAUC,SAAS2B,aAEjCb,aAAaU,eAAgBpC,iCASD,CAACA,KAAMW,YACnCe,aAAahB,mBAAUC,UAAUC,SAASuB,WAAYnC,YAChDwC,eAAiB,CACnB9B,mBAAUC,UAAUC,SAASyB,YAC7B3B,mBAAUC,UAAUC,SAAS2B,aAEjCN,aAAaO,eAAgBxC,aASpByC,cAAiBC,MAIC,IAHb,IAAIlB,iBAAUmB,gBAAOC,UAGtBnB,KAAKiB,gDAQTG,iBAAoBC,cACvBC,YAAcD,MAAM9C,KAAKS,cAAcqC,MAAME,kBAC/CD,cACAA,YAAYE,UAAYH,MAAMI,MAC9BH,YAAYI,aAAa,QAASL,MAAMI,+EAYdJ,WACzBL,cAAcK,MAAMM,QASrBN,MAAMI,MAAQG,UAAUP,MAAMM,YATA,OAIxBE,SAFWR,MAAMM,OAAOG,MAAM,KAEVC,MAAMD,MAAM,KAAK,GAE3CT,MAAMI,MAAQG,UAAUC,UAK5BT,iBAAiBC,6BASOW,eACpBA,UAAUC,SAAS,KAAM,OACnBH,MAAQE,UAAUF,MAAM,SAC1BI,SAAWJ,MAAMA,MAAMK,OAAS,UACpCD,SAAWA,SAASJ,MAAM,KACtBI,SAASC,OAAS,EACXP,UAAUM,SAASE,MAAM,EAAIF,SAASC,OAAS,GAAIE,KAAK,MAExDT,UAAUM,SAAS,WAGvBN,UAAUI,UAAUF,MAAM,KAAK,gCAUZQ,OACvBA,MAAMC,MAAM"}PK/<\x=i:i: amd/build/imageinsert.min.js.mapnu[{"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_media/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_media/imagedetails';\nimport {\n body,\n footer,\n hideElements,\n showElements,\n isValidUrl,\n} from './helpers';\nimport {MAX_LENGTH_ALT} from './imagehelpers';\n\nprefetchStrings('tiny_media', [\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 !== \"\" && isValidUrl(url));\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 templateContext.bodyTemplate = Selectors.IMAGE.template.body.insertImageDetailsBody;\n templateContext.footerTemplate = Selectors.IMAGE.template.footer.insertImageDetailsFooter;\n templateContext.selector = Selectors.IMAGE.type;\n templateContext.maxlengthalt = MAX_LENGTH_ALT;\n\n Promise.all([body(templateContext, this.root), footer(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}\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","url","startImageLoading","currentUrl","image","Image","src","addEventListener","urlWarning","innerHTML","imageurlrequired","stopImageLoading","templateContext","sizecustomhelpicon","text","sizecustom_help","bodyTemplate","template","body","insertImageDetailsBody","footerTemplate","footer","insertImageDetailsFooter","selector","type","maxlengthalt","MAX_LENGTH_ALT","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","urlChanged","input","loadPreviewImage","elementsToHide","insertImage","modalFooter","elementsToShow","params","e","target","closest","imageBrowser","preventDefault","fileInput"],"mappings":"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"}PK/<\6y$$amd/build/selectors.min.js.mapnu[{"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_media/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_image_urlentrysubmit',\n imageBrowser: '.openimagebrowser',\n addUrl: '.tiny_image_addurl',\n deleteImage: '.tiny_image_deleteicon',\n },\n elements: {\n form: 'form.tiny_image_form',\n alignSettings: '.tiny_image_button',\n alt: '.tiny_image_altentry',\n altWarning: '.tiny_image_altwarning',\n height: '.tiny_image_heightentry',\n width: '.tiny_image_widthentry',\n url: '.tiny_image_urlentry',\n urlWarning: '.tiny_image_urlwarning',\n size: '.tiny_image_size',\n presentation: '.tiny_image_presentation',\n constrain: '.tiny_image_constrain',\n customStyle: '.tiny_image_customstyle',\n preview: '.tiny_image_preview',\n previewBox: '.tiny_image_preview_box',\n loaderIcon: '.tiny_image_loader',\n loaderIconContainer: '.tiny_image_loader_container',\n insertImage: '.tiny_image_insert_image',\n modalFooter: '.modal-footer',\n dropzoneContainer: '.tiny_image_dropzone_container',\n fileInput: '#tiny_image_fileinput',\n fileNameLabel: '.tiny_image_filename',\n sizeOriginal: '.tiny_image_sizeoriginal',\n sizeCustom: '.tiny_image_sizecustom',\n properties: '.tiny_image_properties',\n bodyTemplate: '.tiny_image_body_template',\n footerTemplate: '.tiny_image_footer_template',\n },\n styles: {\n responsive: 'img-fluid',\n },\n template: {\n body: {\n insertImageBody: 'tiny_media/insert_image_modal_insert',\n insertImageDetailsBody: 'tiny_media/insert_image_modal_details',\n },\n footer: {\n insertImageFooter: 'tiny_media/insert_image_modal_insert_footer',\n insertImageDetailsFooter: 'tiny_media/insert_image_modal_details_footer',\n },\n },\n type: 'IMAGE',\n },\n EMBED: {\n actions: {\n mediaBrowser: '.openmediabrowser',\n addUrl: '.tiny_media_add_url',\n deleteMedia: '.tiny_media_delete_icon',\n showSubtitleCaption: '[data-action=\"show-subtitle-caption\"]',\n backToMediaDetails: '[data-action=\"back-to-media-details\"]',\n uploadCustomThumbnail: '.upload-custom-thumbnail',\n deleteCustomThumbnail: '.delete-custom-thumbnail',\n deleteThumbnail: '.delete_tiny_thumbnail',\n setPoster: '[data-action=\"set-poster\"]',\n mediaLinkAsAudio: '.link_as_audio',\n mediaLinkAsVideo: '.link_as_video',\n cancelMediaDetails: '[data-action=\"cancel\"]',\n },\n elements: {\n source: '.tiny_media_source',\n track: '.tiny_media_track',\n posterSource: '.tiny_media_poster_source',\n title: '.tiny_media_title_entry',\n url: '.tiny_media_source_url_entry',\n width: '.tiny_media_width_entry',\n height: '.tiny_media_height_entry',\n trackSource: '.tiny_media_track_source',\n trackLabel: '.tiny_media_track_label_entry',\n trackLang: '.tiny_media_track_lang_entry',\n trackDefault: '.tiny_media_track_default',\n mediaControl: '.tiny_media_controls',\n mediaAutoplay: '.tiny_media_autoplay',\n mediaMute: '.tiny_media_mute',\n mediaLoop: '.tiny_media_loop',\n bodyTemplate: '.tiny_media_body_template',\n footerTemplate: '.tiny_media_footer_template',\n dropzoneContainer: '.tiny_media_dropzone_container',\n fromUrl: '.tiny_media_from_url_entry',\n urlWarning: '.tiny_media_url_warning',\n loaderIcon: '.tiny_media_loader',\n loaderIconContainer: '.tiny_media_loader_container',\n insertMedia: '.tiny_media_insert_media',\n modalFooter: '.modal-footer',\n fileNameLabel: '.tiny_media_filename',\n previewBox: '.tiny_media_preview_box',\n sizeOriginal: '.tiny_media_sizeoriginal',\n sizeCustom: '.tiny_media_sizecustom',\n properties: '.tiny_media_properties',\n mediaDetailsBody: '#tiny-media-details-body',\n mediaSubtitleCaptionBody: '#tiny-media-subtitle-caption-body',\n mediaMetadataTabPane: '.tab-pane-media-metadata',\n videoThumbnail: '.video-thumbnail',\n mediaSizeProperties: '.media-size-properties',\n thumbnailPreview: '.tiny_media_thumbnail_preview',\n mediaPreviewContainer: '#media-filter-preview-container',\n },\n template: {\n body: {\n insertMediaBody: 'tiny_media/embed/body/insert_media_body',\n mediaDetailsBody: 'tiny_media/embed/body/media_details_body',\n mediaThumbnailBody: 'tiny_media/embed/body/media_thumbnail_body',\n },\n footer: {\n insertMediaFooter: 'tiny_media/embed/footer/insert_media_footer',\n mediaDetailsFooter: 'tiny_media/embed/footer/media_details_footer',\n mediaThumbnailFooter: 'tiny_media/embed/footer/media_thumbnail_footer',\n },\n },\n type: 'EMBED',\n externalMediaProvider: 'external-media-provider',\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","bodyTemplate","footerTemplate","styles","responsive","template","body","insertImageBody","insertImageDetailsBody","footer","insertImageFooter","insertImageDetailsFooter","type","EMBED","mediaBrowser","deleteMedia","showSubtitleCaption","backToMediaDetails","uploadCustomThumbnail","deleteCustomThumbnail","deleteThumbnail","setPoster","mediaLinkAsAudio","mediaLinkAsVideo","cancelMediaDetails","source","track","posterSource","title","trackSource","trackLabel","trackLang","trackDefault","mediaControl","mediaAutoplay","mediaMute","mediaLoop","fromUrl","insertMedia","mediaDetailsBody","mediaSubtitleCaptionBody","mediaMetadataTabPane","videoThumbnail","mediaSizeProperties","thumbnailPreview","mediaPreviewContainer","insertMediaBody","mediaThumbnailBody","insertMediaFooter","mediaDetailsFooter","mediaThumbnailFooter","externalMediaProvider"],"mappings":"sKAuBe,CACXA,MAAO,CACHC,QAAS,CACLC,OAAQ,6BACRC,aAAc,oBACdC,OAAQ,qBACRC,YAAa,0BAEjBC,SAAU,CACNC,KAAM,uBACNC,cAAe,qBACfC,IAAK,uBACLC,WAAY,yBACZC,OAAQ,0BACRC,MAAO,yBACPC,IAAK,uBACLC,WAAY,yBACZC,KAAM,mBACNC,aAAc,2BACdC,UAAW,wBACXC,YAAa,0BACbC,QAAS,sBACTC,WAAY,0BACZC,WAAY,qBACZC,oBAAqB,+BACrBC,YAAa,2BACbC,YAAa,gBACbC,kBAAmB,iCACnBC,UAAW,wBACXC,cAAe,uBACfC,aAAc,2BACdC,WAAY,yBACZC,WAAY,yBACZC,aAAc,4BACdC,eAAgB,+BAEpBC,OAAQ,CACJC,WAAY,aAEhBC,SAAU,CACNC,KAAM,CACFC,gBAAiB,uCACjBC,uBAAwB,yCAE5BC,OAAQ,CACJC,kBAAmB,8CACnBC,yBAA0B,iDAGlCC,KAAM,SAEVC,MAAO,CACH1C,QAAS,CACL2C,aAAc,oBACdxC,OAAQ,sBACRyC,YAAa,0BACbC,oBAAqB,wCACrBC,mBAAoB,wCACpBC,sBAAuB,2BACvBC,sBAAuB,2BACvBC,gBAAiB,yBACjBC,UAAW,6BACXC,iBAAkB,iBAClBC,iBAAkB,iBAClBC,mBAAoB,0BAExBhD,SAAU,CACNiD,OAAQ,qBACRC,MAAO,oBACPC,aAAc,4BACdC,MAAO,0BACP7C,IAAK,+BACLD,MAAO,0BACPD,OAAQ,2BACRgD,YAAa,2BACbC,WAAY,gCACZC,UAAW,+BACXC,aAAc,4BACdC,aAAc,uBACdC,cAAe,uBACfC,UAAW,mBACXC,UAAW,mBACXnC,aAAc,4BACdC,eAAgB,8BAChBP,kBAAmB,iCACnB0C,QAAS,6BACTrD,WAAY,0BACZO,WAAY,qBACZC,oBAAqB,+BACrB8C,YAAa,2BACb5C,YAAa,gBACbG,cAAe,uBACfP,WAAY,0BACZQ,aAAc,2BACdC,WAAY,yBACZC,WAAY,yBACZuC,iBAAkB,2BAClBC,yBAA0B,oCAC1BC,qBAAsB,2BACtBC,eAAgB,mBAChBC,oBAAqB,yBACrBC,iBAAkB,gCAClBC,sBAAuB,mCAE3BxC,SAAU,CACNC,KAAM,CACFwC,gBAAiB,0CACjBP,iBAAkB,2CAClBQ,mBAAoB,8CAExBtC,OAAQ,CACJuC,kBAAmB,8CACnBC,mBAAoB,+CACpBC,qBAAsB,mDAG9BtC,KAAM,QACNuC,sBAAuB"}PK/<\m3BBamd/build/plugin.min.jsnu[define("tiny_media/plugin",["exports","editor_tiny/loader","editor_tiny/utils","./common","./commands","./configuration","./options"],(function(_exports,_loader,_utils,_common,Commands,Configuration,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 _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_media/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),pluginMetadata))),resolve(["".concat(_common.component,"/plugin"),Configuration])}));return _exports.default=_default,_exports.default})); //# sourceMappingURL=plugin.min.js.mapPK/<\ amd/build/embed.min.jsnu[define("tiny_media/embed",["exports","./embedmodal","./options","editor_tiny/options","./embed/embedhandler","./embed/embedhelpers","./embed/embedinsert","./helpers","./selectors"],(function(_exports,_embedmodal,_options,_options2,_embedhandler,_embedhelpers,_embedinsert,_helpers,_selectors){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,_embedmodal=_interopRequireDefault(_embedmodal),_selectors=_interopRequireDefault(_selectors);return _exports.default=class{constructor(editor){_defineProperty(this,"editor",null),_defineProperty(this,"canShowFilePicker",!1),_defineProperty(this,"canShowFilePickerTrack",!1),_defineProperty(this,"canShowDropZone",!1),_defineProperty(this,"displayDialogue",(async()=>{const[mediaType,selectedMedia]=(0,_embedhelpers.getSelectedMediaElement)(this.editor);this.mediaType=mediaType,this.selectedMedia=selectedMedia,this.selectedMedia?(this.isUpdating=!0,this.loadSelectedMedia()):(await this.createMediaModal(),await this.loadInsertMediaModal())})),_defineProperty(this,"loadInsertMediaModal",(async()=>{const embedHandler=new _embedhandler.EmbedHandler(this);embedHandler.loadTemplatePromise((0,_embedhelpers.insertMediaTemplateContext)(this)),await embedHandler.registerEventListeners()})),_defineProperty(this,"createMediaModal",(async()=>{this.currentModal=await _embedmodal.default.create({large:!0,templateContext:{elementid:this.editor.getElement().id}}),this.modalRoot=this.currentModal.getRoot(),this.root=this.modalRoot[0]})),_defineProperty(this,"loadSelectedMedia",(async()=>{let mediaSource=null;if(["video","audio"].includes(this.mediaType)){mediaSource=this.selectedMedia.querySelector("source").src;const sources=this.selectedMedia.querySelectorAll("source");if(sources.length>1){let alternativeSources=[];Object.keys(sources).forEach((function(source){alternativeSources.push(sources[source].src)})),this.alternativeSources=alternativeSources}}else this.selectedMedia.classList.contains(_selectors.default.EMBED.externalMediaProvider)&&(mediaSource=this.selectedMedia.href,this.mediaType="link");if(this.mediaType){await this.createMediaModal(),(0,_helpers.startMediaLoading)(this.root,_selectors.default.EMBED.type);const embedInsert=new _embedinsert.EmbedInsert(this);embedInsert.init(),embedInsert.loadMediaPreview(mediaSource),await new _embedhandler.EmbedHandler(this).registerEventListeners()}else await this.createMediaModal(),this.loadInsertMediaModal()}));const permissions=(0,_options.getEmbedPermissions)(editor),options=(0,_options2.getFilePicker)(editor,"media");this.canShowFilePicker=permissions.filepicker&&void 0!==options&&Object.keys(options.repositories).length>0,this.canShowFilePickerTrack=permissions.filepicker&&void 0!==(0,_options2.getFilePicker)(editor,"subtitle"),this.canShowDropZone=Object.values(options.repositories).some((repository=>"upload"===repository.type)),this.editor=editor,this.acceptedMediaTypes=options.accepted_types,this.contextId=(0,_options2.getContextId)(editor);const imageOptions=(0,_options2.getFilePicker)(editor,"image");this.acceptedImageTypes=imageOptions.accepted_types,this.canShowImageFilePicker=permissions.filepicker&&void 0!==imageOptions&&Object.keys(imageOptions.repositories).length>0}},_exports.default})); //# sourceMappingURL=embed.min.js.mapPK/<\DH<!amd/build/imagehelpers.min.js.mapnu[{"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_media/imagehelpers\n * @copyright 2024 Meirza \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as Helpers from 'tiny_media/helpers';\nimport {Selectors} from './selectors';\n\n/**\n * Maximum length allowed for the alt attribute.\n */\nexport const MAX_LENGTH_ALT = 750;\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 * @deprecated Since Moodle 5.0 See {@link module:core_editor/tiny/plugins/media/helpers.body}\n */\nexport const bodyImageInsert = async(templateContext, root) => {\n window.console.warn('This function is deprecated. Please use core_editor/tiny/plugins/media/helpers.body instead.');\n\n templateContext.bodyTemplate = 'tiny_media/insert_image_modal_insert';\n templateContext.selector = Selectors.IMAGE.type;\n return Helpers.body(templateContext, root);\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 * @deprecated Since Moodle 5.0 See {@link module:core_editor/tiny/plugins/media/helpers.footer}\n */\nexport const footerImageInsert = async(templateContext, root) => {\n window.console.warn(`This function is deprecated.\n Please use core_editor/tiny/plugins/media/helpers.footer instead.`);\n\n templateContext.footerTemplate = 'tiny_media/insert_image_modal_insert_footer';\n templateContext.selector = Selectors.IMAGE.type;\n return Helpers.footer(templateContext, root);\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 * @deprecated Since Moodle 5.0 See {@link module:core_editor/tiny/plugins/media/helpers.body}\n */\nexport const bodyImageDetails = async(templateContext, root) => {\n window.console.warn(`This function is deprecated.\n Please use core_editor/tiny/plugins/media/helpers.body instead.`);\n\n templateContext.bodyTemplate = 'tiny_media/insert_image_modal_details';\n templateContext.selector = Selectors.IMAGE.type;\n return Helpers.body(templateContext, root);\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 * @deprecated Since Moodle 5.0 See {@link module:core_editor/tiny/plugins/media/helpers.footer}\n */\nexport const footerImageDetails = async(templateContext, root) => {\n window.console.warn(`This function is deprecated.\n Please use core_editor/tiny/plugins/media/helpers.footer instead.`);\n\n templateContext.footerTemplate = 'tiny_media/insert_image_modal_details_footer';\n templateContext.selector = Selectors.IMAGE.type;\n return Helpers.footer(templateContext, root);\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 * @deprecated Since Moodle 5.0 See {@link module:core_editor/tiny/plugins/media/helpers.showElements}\n */\nexport const showElements = (elements, root) => {\n window.console.warn(`This function is deprecated.\n Please use core_editor/tiny/plugins/media/helpers.showElements instead.`);\n Helpers.showElements(elements, root);\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 * @deprecated Since Moodle 5.0 See {@link module:core_editor/tiny/plugins/media/helpers.hideElements}\n */\nexport const hideElements = (elements, root) => {\n window.console.warn(`This function is deprecated.\n Please use core_editor/tiny/plugins/media/helpers.hideElements instead.`);\n Helpers.hideElements(elements, root);\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 * @deprecated Since Moodle 5.0 See {@link module:core_editor/tiny/plugins/media/helpers.isPercentageValue}\n */\nexport const isPercentageValue = (value) => {\n window.console.warn(`This function is deprecated.\n Please use core_editor/tiny/plugins/media/helpers.isPercentageValue instead.`);\n return Helpers.isPercentageValue(value);\n};\n"],"names":["async","templateContext","root","window","console","warn","bodyTemplate","selector","Selectors","IMAGE","type","Helpers","body","footerTemplate","footer","elements","showElements","hideElements","value","isPercentageValue"],"mappings":";;;;;;;uCA6B8B,6BAUCA,MAAMC,gBAAiBC,QAClDC,OAAOC,QAAQC,KAAK,gGAEpBJ,gBAAgBK,aAAe,uCAC/BL,gBAAgBM,SAAWC,qBAAUC,MAAMC,KACpCC,QAAQC,KAAKX,gBAAiBC,kCAWRF,MAAMC,gBAAiBC,QACpDC,OAAOC,QAAQC,gHAGfJ,gBAAgBY,eAAiB,8CACjCZ,gBAAgBM,SAAWC,qBAAUC,MAAMC,KACpCC,QAAQG,OAAOb,gBAAiBC,iCAWXF,MAAMC,gBAAiBC,QACnDC,OAAOC,QAAQC,8GAGfJ,gBAAgBK,aAAe,wCAC/BL,gBAAgBM,SAAWC,qBAAUC,MAAMC,KACpCC,QAAQC,KAAKX,gBAAiBC,mCAUPF,MAAMC,gBAAiBC,QACrDC,OAAOC,QAAQC,gHAGfJ,gBAAgBY,eAAiB,+CACjCZ,gBAAgBM,SAAWC,qBAAUC,MAAMC,KACpCC,QAAQG,OAAOb,gBAAiBC,6BAUf,CAACa,SAAUb,QACnCC,OAAOC,QAAQC,sHAEfM,QAAQK,aAAaD,SAAUb,6BAUP,CAACa,SAAUb,QACnCC,OAAOC,QAAQC,sHAEfM,QAAQM,aAAaF,SAAUb,kCAUDgB,QAC9Bf,OAAOC,QAAQC,2HAERM,QAAQQ,kBAAkBD"}PK/<\#İVV"amd/build/configuration.min.js.mapnu[{"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_media/configuration\n * @copyright 2022 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {\n imageButtonName,\n videoButtonName,\n mediaManagerButtonName,\n} from './common';\nimport uploadFile from 'editor_tiny/uploader';\nimport {\n addContextmenuItem,\n} from 'editor_tiny/utils';\n\nconst configureMenu = (menu) => {\n // Replace the standard Media plugin with the Moodle embed.\n if (menu.insert.items.match(/\\bmedia\\b/)) {\n menu.insert.items = menu.insert.items.replace(/\\bmedia\\b/, videoButtonName);\n } else {\n menu.insert.items = `${videoButtonName} ${menu.insert.items}`;\n }\n\n // Replace the standard image plugin with the Moodle image.\n if (menu.insert.items.match(/\\bimage\\b/)) {\n menu.insert.items = menu.insert.items.replace(/\\bimage\\b/, imageButtonName);\n } else {\n menu.insert.items = `${imageButtonName} ${menu.insert.items}`;\n }\n\n // Add the Media Manager to the end of the Tools menu.\n menu.tools.items += ` ${mediaManagerButtonName}`;\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 image, and embed, buttons at the start of it.\n section.items.unshift(imageButtonName, videoButtonName);\n }\n\n return section;\n });\n};\n\nexport const configure = (instanceConfig) => {\n // Update the instance configuration to add the Media menu option to the menus and toolbars and upload_handler.\n return {\n contextmenu: addContextmenuItem(instanceConfig.contextmenu, imageButtonName, videoButtonName),\n menu: configureMenu(instanceConfig.menu),\n toolbar: configureToolbar(instanceConfig.toolbar),\n\n // eslint-disable-next-line camelcase\n images_upload_handler: (blobInfo, progress) => uploadFile(\n window.tinymce.activeEditor,\n 'image',\n blobInfo.blob(),\n blobInfo.filename(),\n progress\n ),\n\n // eslint-disable-next-line camelcase\n images_reuse_filename: true,\n };\n};\n"],"names":["instanceConfig","contextmenu","imageButtonName","videoButtonName","menu","insert","items","match","replace","tools","mediaManagerButtonName","toolbar","map","section","name","unshift","images_upload_handler","blobInfo","progress","window","tinymce","activeEditor","blob","filename","images_reuse_filename"],"mappings":";;;;;;;sKAoE0BA,uBAEf,CACHC,aAAa,6BAAmBD,eAAeC,YAAaC,wBAAiBC,yBAC7EC,MAvCeA,KAuCKJ,eAAeI,KArCnCA,KAAKC,OAAOC,MAAMC,MAAM,aACxBH,KAAKC,OAAOC,MAAQF,KAAKC,OAAOC,MAAME,QAAQ,YAAaL,yBAE3DC,KAAKC,OAAOC,gBAAWH,oCAAmBC,KAAKC,OAAOC,OAItDF,KAAKC,OAAOC,MAAMC,MAAM,aACxBH,KAAKC,OAAOC,MAAQF,KAAKC,OAAOC,MAAME,QAAQ,YAAaN,yBAE3DE,KAAKC,OAAOC,gBAAWJ,oCAAmBE,KAAKC,OAAOC,OAI1DF,KAAKK,MAAMH,kBAAaI,gCAEjBN,MAsBHO,SAnBkBA,QAmBQX,eAAeW,QAftCA,QAAQC,KAAKC,UACK,YAAjBA,QAAQC,MAERD,QAAQP,MAAMS,QAAQb,wBAAiBC,yBAGpCU,YAYPG,sBAAuB,CAACC,SAAUC,YAAa,qBAC3CC,OAAOC,QAAQC,aACf,QACAJ,SAASK,OACTL,SAASM,WACTL,UAIJM,uBAAuB,GA/BLb,IAAAA,QArBHP"}PK/<\si--/amd/build/embed/embedthumbnailinsert.min.js.mapnu[{"version":3,"file":"embedthumbnailinsert.min.js","sources":["../../src/embed/embedthumbnailinsert.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 thumbnail upload class.\n *\n * This handles the embed thumbnail upload using drag-drop.\n *\n * @module tiny_media/embed/embedthumbnailinsert\n * @copyright 2024 Stevani Andolo \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 {\n showElements,\n startMediaLoading,\n stopMediaLoading,\n setPropertiesFromData,\n body,\n footer,\n} from '../helpers';\nimport {EmbedThumbnailPreview} from './embedthumbnailpreview';\nimport {EmbedHandler} from './embedhandler';\nimport {displayFilepicker} from 'editor_tiny/utils';\n\nprefetchStrings(component, [\n 'insertmediathumbnail',\n 'uploading',\n 'loadingembedthumbnail',\n 'addmediathumbnaildrop',\n]);\n\nexport class EmbedThumbnailInsert {\n\n constructor(data) {\n setPropertiesFromData(this, data); // Creates dynamic properties based on \"data\" param.\n }\n\n /**\n * Init the dropzone and lang strings.\n *\n * @param {object} mediaData Object of selected media data\n */\n init = async(mediaData) => {\n this.mediaData = mediaData; // Current selected media data passed from embedPreview.\n const langStringKeys = [\n 'insertmediathumbnail',\n 'uploading',\n 'loadingembedthumbnail',\n 'addmediathumbnaildrop',\n ];\n const langStringvalues = await getStrings([...langStringKeys].map((key) => ({key, component})));\n this.langStrings = Object.fromEntries(langStringKeys.map((key, index) => [key, langStringvalues[index]]));\n this.currentModal.uploadThumbnailModal.setTitle(this.langStrings.insertmediathumbnail);\n\n // Let's init the dropzone if canShowDropZone is true.\n if (this.canShowDropZone) {\n const dropZoneEle = document.querySelector(Selectors.EMBED.elements.dropzoneContainer);\n const dropZone = new Dropzone(\n dropZoneEle,\n this.acceptedImageTypes,\n files => {\n this.handleUploadedFile(files);\n }\n );\n dropZone.setLabel(this.langStrings.addmediathumbnaildrop);\n dropZone.init();\n }\n\n this.registerInsertMediaThumbnailEvents(this.thumbnailModalRoot);\n };\n\n /**\n * Load and display a preview thumbnail based on the provided URL, and handles thumbnail loading events.\n *\n * @param {string} url - The URL of the thumbnail to load and display.\n */\n loadPreviewThumbnail = (url) => {\n this.media.poster = url;\n\n let templateContext = {\n bodyTemplate: Selectors.EMBED.template.body.mediaThumbnailBody,\n footerTemplate: Selectors.EMBED.template.footer.mediaThumbnailFooter,\n selector: Selectors.EMBED.type,\n };\n\n Promise.all([body(templateContext, this.thumbnailModalRoot), footer(templateContext, this.thumbnailModalRoot)])\n .then(() => {\n const mediaThumbnail = new EmbedThumbnailPreview(this);\n mediaThumbnail.init(this.mediaData);\n return;\n })\n .catch(error => {\n window.console.log(error);\n });\n };\n\n /**\n * Handles media preview on file picker callback.\n *\n * @param {object} params Object of uploaded file\n */\n filePickerCallback = (params) => {\n if (params.url) {\n this.loadPreviewThumbnail(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 */\n updateLoaderIcon = (root, langStrings, progress = null) => {\n const loaderIconState = root.querySelector(Selectors.EMBED.elements.loaderIconContainer + ' div');\n loaderIconState.innerHTML = (progress !== null) ?\n `${langStrings.uploading} ${Math.round(progress)}%` :\n langStrings.loadingembedthumbnail;\n };\n\n /**\n * Handles changes in the media URL input field and loads a preview of the media if the URL has changed.\n */\n urlChanged() {\n const url = this.thumbnailModalRoot.querySelector(Selectors.EMBED.elements.fromUrl).value;\n if (url && url !== this.currentUrl) {\n this.loadPreviewThumbnail(url);\n }\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 startMediaLoading(this.thumbnailModalRoot, Selectors.EMBED.type);\n const fileURL = await uploadFile(this.editor, 'image', files[0], files[0].name, (progress) => {\n this.updateLoaderIcon(this.thumbnailModalRoot, this.langStrings, progress);\n });\n\n // Set the loader icon content to \"loading\" after the file upload completes.\n this.updateLoaderIcon(this.thumbnailModalRoot, this.langStrings);\n this.filePickerCallback({url: fileURL});\n } catch (error) {\n // Handle the error.\n const urlWarningLabelEle = this.thumbnailModalRoot.querySelector(Selectors.EMBED.elements.urlWarning);\n urlWarningLabelEle.innerHTML = error.error !== undefined ? error.error : error;\n showElements(Selectors.EMBED.elements.urlWarning, this.thumbnailModalRoot);\n stopMediaLoading(this.thumbnailModalRoot, Selectors.EMBED.type);\n }\n };\n\n /**\n * Registers events for insert thumbnail modal.\n *\n * @param {HTMLElement} root - The root element containing the loader icon.\n */\n registerInsertMediaThumbnailEvents = (root) => {\n const urlEle = root.querySelector(Selectors.EMBED.elements.fromUrl);\n if (urlEle) {\n urlEle.addEventListener('input', () => {\n (new EmbedHandler(this)).toggleUrlButton(urlEle, this.thumbnailModalRoot);\n });\n }\n\n // Handles add media url.\n const addUrlEle = root.querySelector(Selectors.EMBED.actions.addUrl);\n if (addUrlEle) {\n addUrlEle.addEventListener('click', () => {\n startMediaLoading(this.thumbnailModalRoot, Selectors.EMBED.type);\n this.urlChanged();\n });\n }\n\n // Handle repository browsing.\n const imageBrowser = root.querySelector(Selectors.IMAGE.actions.imageBrowser);\n if (imageBrowser) {\n imageBrowser.addEventListener('click', async(e) => {\n e.preventDefault();\n const params = await displayFilepicker(this.editor, 'image');\n this.filePickerCallback(params);\n });\n }\n };\n}\n"],"names":["component","constructor","data","async","mediaData","langStringKeys","langStringvalues","map","key","langStrings","Object","fromEntries","index","currentModal","uploadThumbnailModal","setTitle","this","insertmediathumbnail","canShowDropZone","dropZoneEle","document","querySelector","Selectors","EMBED","elements","dropzoneContainer","dropZone","Dropzone","acceptedImageTypes","files","handleUploadedFile","setLabel","addmediathumbnaildrop","init","registerInsertMediaThumbnailEvents","thumbnailModalRoot","url","media","poster","templateContext","bodyTemplate","template","body","mediaThumbnailBody","footerTemplate","footer","mediaThumbnailFooter","selector","type","Promise","all","then","EmbedThumbnailPreview","catch","error","window","console","log","params","loadPreviewThumbnail","root","progress","loaderIconState","loaderIconContainer","innerHTML","uploading","Math","round","loadingembedthumbnail","fileURL","editor","name","updateLoaderIcon","filePickerCallback","urlWarning","undefined","urlEle","fromUrl","addEventListener","EmbedHandler","toggleUrlButton","addUrlEle","actions","addUrl","urlChanged","imageBrowser","IMAGE","e","preventDefault","value","currentUrl"],"mappings":"01BA2CgBA,kBAAW,CACvB,uBACA,YACA,wBACA,8DAKAC,YAAYC,mCASLC,MAAAA,iBACEC,UAAYA,gBACXC,eAAiB,CACnB,uBACA,YACA,wBACA,yBAEEC,uBAAyB,mBAAW,IAAID,gBAAgBE,KAAKC,OAAUA,IAAAA,IAAKR,UAAAA,+BAC7ES,YAAcC,OAAOC,YAAYN,eAAeE,KAAI,CAACC,IAAKI,QAAU,CAACJ,IAAKF,iBAAiBM,gBAC3FC,aAAaC,qBAAqBC,SAASC,KAAKP,YAAYQ,sBAG7DD,KAAKE,gBAAiB,OAChBC,YAAcC,SAASC,cAAcC,mBAAUC,MAAMC,SAASC,mBAC9DC,SAAW,IAAIC,kBACjBR,YACAH,KAAKY,oBACLC,aACSC,mBAAmBD,UAGhCH,SAASK,SAASf,KAAKP,YAAYuB,uBACnCN,SAASO,YAGRC,mCAAmClB,KAAKmB,oEAQzBC,WACfC,MAAMC,OAASF,QAEhBG,gBAAkB,CAClBC,aAAclB,mBAAUC,MAAMkB,SAASC,KAAKC,mBAC5CC,eAAgBtB,mBAAUC,MAAMkB,SAASI,OAAOC,qBAChDC,SAAUzB,mBAAUC,MAAMyB,MAG9BC,QAAQC,IAAI,EAAC,iBAAKX,gBAAiBvB,KAAKmB,qBAAqB,mBAAOI,gBAAiBvB,KAAKmB,sBACrFgB,MAAK,KACqB,IAAIC,6CAAsBpC,MAClCiB,KAAKjB,KAAKZ,cAG5BiD,OAAMC,QACHC,OAAOC,QAAQC,IAAIH,wDASTI,SACdA,OAAOtB,UACFuB,qBAAqBD,OAAOtB,iDAWtB,SAACwB,KAAMnD,iBAAaoD,gEAAW,WACxCC,gBAAkBF,KAAKvC,cAAcC,mBAAUC,MAAMC,SAASuC,oBAAsB,QAC1FD,gBAAgBE,UAA0B,OAAbH,mBACHpD,YAAYwD,sBAAaC,KAAKC,MAAMN,eACvCpD,YAAY2D,oEAmBlBjE,MAAAA,2CAEKa,KAAKmB,mBAAoBb,mBAAUC,MAAMyB,YACrDqB,cAAgB,qBAAWrD,KAAKsD,OAAQ,QAASzC,MAAM,GAAIA,MAAM,GAAG0C,MAAOV,gBACxEW,iBAAiBxD,KAAKmB,mBAAoBnB,KAAKP,YAAaoD,kBAIhEW,iBAAiBxD,KAAKmB,mBAAoBnB,KAAKP,kBAC/CgE,mBAAmB,CAACrC,IAAKiC,UAChC,MAAOf,OAEsBtC,KAAKmB,mBAAmBd,cAAcC,mBAAUC,MAAMC,SAASkD,YACvEV,eAA4BW,IAAhBrB,MAAMA,MAAsBA,MAAMA,MAAQA,gCAC5DhC,mBAAUC,MAAMC,SAASkD,WAAY1D,KAAKmB,kDACtCnB,KAAKmB,mBAAoBb,mBAAUC,MAAMyB,qEAS5BY,aAC5BgB,OAAShB,KAAKvC,cAAcC,mBAAUC,MAAMC,SAASqD,SACvDD,QACAA,OAAOE,iBAAiB,SAAS,SACxBC,2BAAa/D,MAAOgE,gBAAgBJ,OAAQ5D,KAAKmB,6BAKxD8C,UAAYrB,KAAKvC,cAAcC,mBAAUC,MAAM2D,QAAQC,QACzDF,WACAA,UAAUH,iBAAiB,SAAS,oCACd9D,KAAKmB,mBAAoBb,mBAAUC,MAAMyB,WACtDoC,sBAKPC,aAAezB,KAAKvC,cAAcC,mBAAUgE,MAAMJ,QAAQG,cAC5DA,cACAA,aAAaP,iBAAiB,SAAS3E,MAAAA,IACnCoF,EAAEC,uBACI9B,aAAe,4BAAkB1C,KAAKsD,OAAQ,cAC/CG,mBAAmBf,iDAtJV1C,KAAMd,MA0FhCkF,mBACUhD,IAAMpB,KAAKmB,mBAAmBd,cAAcC,mBAAUC,MAAMC,SAASqD,SAASY,MAChFrD,KAAOA,MAAQpB,KAAK0E,iBACf/B,qBAAqBvB"}PK/<\۱l..&amd/build/embed/embedinsert.min.js.mapnu[{"version":3,"file":"embedinsert.min.js","sources":["../../src/embed/embedinsert.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 upload class.\n *\n * This handles the embed upload using url, drag-drop and repositories.\n *\n * @module tiny_media/embed/embedinsert\n * @copyright 2024 Stevani Andolo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {prefetchStrings} from 'core/prefetch';\nimport {getStrings} from 'core/str';\nimport {component} from \"../common\";\nimport {\n setPropertiesFromData,\n startMediaLoading,\n stopMediaLoading,\n showElements,\n} from '../helpers';\nimport Selectors from \"../selectors\";\nimport Dropzone from 'core/dropzone';\nimport uploadFile from 'editor_tiny/uploader';\nimport {EmbedHandler} from './embedhandler';\nimport {\n getMediaTitle,\n mediaDetailsTemplateContext,\n checkMediaType,\n fetchPreview,\n} from './embedhelpers';\nimport {EmbedPreview} from './embedpreview';\n\nprefetchStrings(component, [\n 'insertmedia',\n 'addmediafilesdrop',\n 'uploading',\n 'loadingmedia',\n]);\n\nexport class EmbedInsert {\n\n constructor(data) {\n setPropertiesFromData(this, data); // Creates dynamic properties based on \"data\" param.\n }\n\n /**\n * Init the dropzone and lang strings.\n */\n init = async() => {\n const langStringKeys = [\n 'insertmedia',\n 'addmediafilesdrop',\n 'uploading',\n 'loadingmedia',\n ];\n const langStringValues = await getStrings([...langStringKeys].map((key) => ({key, component})));\n this.langStrings = Object.fromEntries(langStringKeys.map((key, index) => [key, langStringValues[index]]));\n this.currentModal.setTitle(this.langStrings.insertmedia);\n\n // Let's init the dropzone if canShowDropZone is true and mediaType is null.\n if (this.canShowDropZone && !this.mediaType) {\n const dropZoneEle = document.querySelector(Selectors.EMBED.elements.dropzoneContainer);\n const dropZone = new Dropzone(\n dropZoneEle,\n this.acceptedMediaTypes,\n files => {\n this.handleUploadedFile(files);\n }\n );\n\n dropZone.setLabel(this.langStrings.addmediafilesdrop);\n dropZone.init();\n }\n };\n\n /**\n * Loads and displays a preview media based on the provided URL, and handles media loading events.\n *\n * @param {string} url - The URL of the media to load and display.\n */\n loadMediaPreview = async(url) => {\n const parsedUrl = new URL(url);\n this.fetchedMediaLinkTitle = await getMediaTitle(url, this);\n // The hash in a URL can cause issues. Remove it.\n this.originalUrl = parsedUrl.hash ? parsedUrl.toString().split('#')[0] : parsedUrl.toString();\n\n if (this.newMediaLink) { // Media added using url input.\n this.filteredContent = await fetchPreview(this.originalUrl, this.contextId);\n\n if (!this.mediaType) {\n // If the filtered content is not empty, it means Moodle's filter recognized it as external media.\n if (this.filteredContent && this.filteredContent.trim() !== '') {\n this.mediaType = 'link';\n } else if (this.fetchedMediaLinkTitle) {\n // It means the url points to a physical media file with a hash-based title.\n // Case-insensitive regex for video tag.\n const videoRegex = /]*>.*<\\/video>/i;\n // Case-insensitive regex for audio tag.\n const audioRegex = /]*>.*<\\/audio>/i;\n\n if (videoRegex.test(this.filteredContent)) {\n this.mediaType = 'video';\n } else if (audioRegex.test(this.filteredContent)) {\n this.mediaType = 'audio';\n }\n }\n }\n\n // Process the media preview.\n this.processMediaPreview();\n } else { // Media added using dropzone or repositories.\n this.mediaType ??= await checkMediaType(url);\n\n // If checkMediaType returns null, try fetching preview to detect external media providers.\n if (!this.mediaType) {\n this.filteredContent = await fetchPreview(this.originalUrl, this.contextId);\n // If the filtered content is not empty, it means Moodle's filter recognized it as external media.\n if (this.filteredContent && this.filteredContent.trim() !== '') {\n this.mediaType = 'link';\n }\n }\n\n // Process the media preview.\n this.processMediaPreview();\n }\n };\n\n /**\n * Process the media preview.\n */\n processMediaPreview = async() => {\n // Let's combine the props.\n setPropertiesFromData(\n this,\n await (new EmbedHandler(this)).getMediaTemplateContext()\n );\n\n // Construct templateContext for embed preview.\n const templateContext = await mediaDetailsTemplateContext(this);\n\n if (this.isUpdating && !this.newMediaLink) {\n // Will be used to set the media title if it's in update state.\n this.mediaTitle = templateContext.media.title;\n }\n\n // Load the media details and preview of the selected media.\n (new EmbedHandler(this)).loadMediaDetails(new EmbedPreview(this), templateContext);\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 loaderIconState = root.querySelector(Selectors.EMBED.elements.loaderIconContainer + ' div');\n loaderIconState.innerHTML = (progress !== null) ?\n `${langStrings.uploading} ${Math.round(progress)}%` :\n langStrings.loadingmedia;\n };\n\n /**\n * Handles media preview on file picker callback.\n *\n * @param {object} params Object of uploaded file\n */\n filePickerCallback = (params) => {\n if (params.url) {\n if (this.mediaType) {\n // Set mediaType to \"null\" if it started with viewing embedded link, otherwise it will not be consistent.\n this.mediaType = null;\n }\n\n // Flag as new file upload.\n this.newFileUpload = true;\n\n // Load the media preview.\n this.loadMediaPreview(params.url);\n }\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 startMediaLoading(this.root, Selectors.EMBED.type);\n const fileURL = await uploadFile(this.editor, 'media', files[0], files[0].name, (progress) => {\n this.updateLoaderIcon(this.root, this.langStrings, progress);\n });\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.EMBED.elements.urlWarning);\n urlWarningLabelEle.innerHTML = error.error !== undefined ? error.error : error;\n showElements(Selectors.EMBED.elements.urlWarning, this.root);\n stopMediaLoading(this.root, Selectors.EMBED.type);\n }\n };\n}\n"],"names":["component","constructor","data","async","langStringKeys","langStringValues","map","key","langStrings","Object","fromEntries","index","currentModal","setTitle","this","insertmedia","canShowDropZone","mediaType","dropZoneEle","document","querySelector","Selectors","EMBED","elements","dropzoneContainer","dropZone","Dropzone","acceptedMediaTypes","files","handleUploadedFile","setLabel","addmediafilesdrop","init","parsedUrl","URL","url","fetchedMediaLinkTitle","originalUrl","hash","toString","split","newMediaLink","filteredContent","contextId","trim","audioRegex","test","processMediaPreview","EmbedHandler","getMediaTemplateContext","templateContext","isUpdating","mediaTitle","media","title","loadMediaDetails","EmbedPreview","root","progress","loaderIconState","loaderIconContainer","innerHTML","uploading","Math","round","loadingmedia","params","newFileUpload","loadMediaPreview","type","fileURL","editor","name","updateLoaderIcon","filePickerCallback","error","urlWarning","undefined"],"mappings":"0zBA8CgBA,kBAAW,CACvB,cACA,oBACA,YACA,4CAKAC,YAAYC,mCAOLC,gBACGC,eAAiB,CACnB,cACA,oBACA,YACA,gBAEEC,uBAAyB,mBAAW,IAAID,gBAAgBE,KAAKC,OAAUA,IAAAA,IAAKP,UAAAA,+BAC7EQ,YAAcC,OAAOC,YAAYN,eAAeE,KAAI,CAACC,IAAKI,QAAU,CAACJ,IAAKF,iBAAiBM,gBAC3FC,aAAaC,SAASC,KAAKN,YAAYO,aAGxCD,KAAKE,kBAAoBF,KAAKG,UAAW,OACnCC,YAAcC,SAASC,cAAcC,mBAAUC,MAAMC,SAASC,mBAC9DC,SAAW,IAAIC,kBACjBR,YACAJ,KAAKa,oBACLC,aACSC,mBAAmBD,UAIhCH,SAASK,SAAShB,KAAKN,YAAYuB,mBACnCN,SAASO,oDASE7B,MAAAA,YACT8B,UAAY,IAAIC,IAAIC,aACrBC,4BAA8B,+BAAcD,IAAKrB,WAEjDuB,YAAcJ,UAAUK,KAAOL,UAAUM,WAAWC,MAAM,KAAK,GAAKP,UAAUM,WAE/EzB,KAAK2B,aAAc,SACdC,sBAAwB,8BAAa5B,KAAKuB,YAAavB,KAAK6B,YAE5D7B,KAAKG,aAEFH,KAAK4B,iBAAmD,KAAhC5B,KAAK4B,gBAAgBE,YACxC3B,UAAY,YACd,GAAIH,KAAKsB,sBAAuB,OAK7BS,WAAa,2BAFA,2BAIJC,KAAKhC,KAAK4B,sBAChBzB,UAAY,QACV4B,WAAWC,KAAKhC,KAAK4B,wBACvBzB,UAAY,cAMxB8B,0BACF,kDACE9B,4CAAAA,gBAAoB,gCAAekB,MAGnCrB,KAAKG,iBACDyB,sBAAwB,8BAAa5B,KAAKuB,YAAavB,KAAK6B,WAE7D7B,KAAK4B,iBAAmD,KAAhC5B,KAAK4B,gBAAgBE,cACxC3B,UAAY,cAKpB8B,sEAOS5C,6CAGdW,WACO,IAAIkC,2BAAalC,MAAOmC,iCAI7BC,sBAAwB,6CAA4BpC,MAEtDA,KAAKqC,aAAerC,KAAK2B,oBAEpBW,WAAaF,gBAAgBG,MAAMC,WAIvCN,2BAAalC,MAAOyC,iBAAiB,IAAIC,2BAAa1C,MAAOoC,6DAWnD,SAACO,KAAMjD,iBAAakD,gEAAW,WACxCC,gBAAkBF,KAAKrC,cAAcC,mBAAUC,MAAMC,SAASqC,oBAAsB,QAC1FD,gBAAgBE,UAA0B,OAAbH,mBACHlD,YAAYsD,sBAAaC,KAAKC,MAAMN,eACvClD,YAAYyD,2DAQjBC,SACdA,OAAO/B,MACHrB,KAAKG,iBAEAA,UAAY,WAIhBkD,eAAgB,OAGhBC,iBAAiBF,OAAO/B,oDAUhBhC,MAAAA,2CAEKW,KAAK2C,KAAMpC,mBAAUC,MAAM+C,YACvCC,cAAgB,qBAAWxD,KAAKyD,OAAQ,QAAS3C,MAAM,GAAIA,MAAM,GAAG4C,MAAOd,gBACxEe,iBAAiB3D,KAAK2C,KAAM3C,KAAKN,YAAakD,kBAIlDe,iBAAiB3D,KAAK2C,KAAM3C,KAAKN,kBACjCkE,mBAAmB,CAACvC,IAAKmC,UAChC,MAAOK,OAEsB7D,KAAK2C,KAAKrC,cAAcC,mBAAUC,MAAMC,SAASqD,YACzDf,eAA4BgB,IAAhBF,MAAMA,MAAsBA,MAAMA,MAAQA,gCAC5DtD,mBAAUC,MAAMC,SAASqD,WAAY9D,KAAK2C,oCACtC3C,KAAK2C,KAAMpC,mBAAUC,MAAM+C,6CAnK1BvD,KAAMZ"}PK/<\oQ9Q9'amd/build/embed/embedhelpers.min.js.mapnu[{"version":3,"file":"embedhelpers.min.js","sources":["../../src/embed/embedhelpers.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 helpers.\n *\n * This provides easy access to any classes without instantiating a new object.\n *\n * @module tiny_media/embed/embedhelpers\n * @copyright 2024 Stevani Andolo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Selectors from '../selectors';\nimport {getStrings} from 'core/str';\nimport {component} from \"../common\";\nimport {getCurrentLanguage, getMoodleLang} from 'editor_tiny/options';\nimport Ajax from 'core/ajax';\nimport {getFileName} from '../helpers';\n\n/**\n * Return template context for insert media.\n *\n * @param {object} props\n * @returns {object}\n */\nexport const insertMediaTemplateContext = (props) => {\n return {\n mediaType: props.mediaType,\n showDropzone: props.canShowDropZone,\n showFilePicker: props.canShowFilePicker,\n fileType: 'audio/video',\n };\n};\n\n/**\n * Return template context for insert media.\n *\n * @param {object} props\n * @returns {object}\n */\nexport const insertMediaThumbnailTemplateContext = (props) => {\n return {\n elementid: props.editor.id,\n showDropzone: props.canShowDropZone,\n showImageFilePicker: props.canShowImageFilePicker,\n bodyTemplate: Selectors.EMBED.template.body.insertMediaBody,\n footerTemplate: Selectors.EMBED.template.footer.insertMediaFooter,\n fileType: 'image',\n selector: Selectors.EMBED.type,\n };\n};\n\n/**\n * Return selected media type and element.\n *\n * @param {editor} editor\n * @returns {Array}\n */\nexport const getSelectedMediaElement = (editor) => {\n let mediaType = null;\n let selectedMedia = null;\n const mediaElm = editor.selection.getNode();\n\n if (!mediaElm) {\n mediaType = null;\n selectedMedia = null;\n } else if (mediaElm.nodeName.toLowerCase() === 'video' || mediaElm.nodeName.toLowerCase() === 'audio') {\n mediaType = mediaElm.nodeName.toLowerCase();\n selectedMedia = mediaElm;\n } else if (mediaElm.querySelector('video')) {\n mediaType = 'video';\n selectedMedia = mediaElm.querySelector('video');\n } else if (mediaElm.querySelector('audio')) {\n mediaType = 'audio';\n selectedMedia = mediaElm.querySelector('audio');\n } else if (mediaElm.nodeName.toLowerCase() === 'a') {\n selectedMedia = mediaElm;\n }\n\n return [mediaType, selectedMedia];\n};\n\n/**\n * Returns result of media filtering.\n *\n * @param {string} url\n * @param {string} contextId\n * @returns {string}\n */\nexport const fetchPreview = async(url, contextId) => {\n const request = {\n methodname: 'tiny_media_preview',\n args: {\n contextid: contextId, // Use the system one.\n content: url,\n }\n };\n const responseObj = await Ajax.call([request])[0];\n return responseObj.content;\n};\n\n/**\n * Returns media type.\n *\n * @param {string} url\n * @returns {string|null}\n */\nexport const checkMediaType = async(url) => {\n try {\n const response = await fetch(url, {method: 'HEAD'});\n const contentType = response.headers.get('Content-type');\n\n if (!contentType) {\n return null;\n }\n\n if (contentType.startsWith('video/')) {\n return 'video';\n } else if (contentType.startsWith('audio/')) {\n return 'audio';\n }\n\n return null;\n } catch (e) {\n return null;\n }\n};\n\n/**\n * Returns media title.\n *\n * @param {string} url\n * @param {object} props\n * @returns {string|null} String of media title.\n */\nexport const getMediaTitle = async(url, props) => {\n const parsedUrl = new URL(url);\n\n // Try get the title via the file name.\n const pathName = parsedUrl.pathname;\n const extension = pathName.includes('.') ? pathName.split('.').pop() : null;\n if (extension && props.acceptedMediaTypes.includes(`.${extension}`)) {\n return getFileName(url);\n }\n\n // Try and get the title from the value after the '#'.\n const hashTitle = parsedUrl.hash ? parsedUrl.hash.substring(1) : null;\n if (hashTitle) {\n return hashTitle;\n }\n\n return null;\n};\n\n/**\n * Return template context for media details.\n *\n * @param {object} props\n * @returns {object}\n */\nexport const mediaDetailsTemplateContext = async(props) => {\n const context = {\n bodyTemplate: Selectors.EMBED.template.body.mediaDetailsBody,\n footerTemplate: Selectors.EMBED.template.footer.mediaDetailsFooter,\n isLink: (props.mediaType === 'link'),\n isVideo: (props.mediaType === 'video'),\n showControl: (props.mediaType === 'video' || props.mediaType === 'audio'),\n isUpdating: props.isUpdating,\n isNewFileOrLinkUpload: (props.newMediaLink || props.newFileUpload),\n selector: Selectors.EMBED.type,\n };\n\n return {...context, ...props};\n};\n\n/**\n * Get help strings.\n *\n * @returns {object}\n */\nexport const getHelpStrings = async() => {\n const [\n subtitles,\n captions,\n descriptions,\n chapters,\n metadata,\n customsize,\n linkcustomsize,\n ] = await getStrings([\n 'subtitles_help',\n 'captions_help',\n 'descriptions_help',\n 'chapters_help',\n 'metadata_help',\n 'customsize_help',\n 'linkcustomsize_help',\n ].map((key) => ({\n key,\n component,\n })));\n\n return {\n subtitles,\n captions,\n descriptions,\n chapters,\n metadata,\n customsize,\n linkcustomsize,\n };\n};\n\n/**\n * Get current moodle languages.\n *\n * @param {editor} editor\n * @returns {object}\n */\nexport const prepareMoodleLang = (editor) => {\n const moodleLangs = getMoodleLang(editor);\n const currentLanguage = getCurrentLanguage(editor);\n\n const installed = Object.entries(moodleLangs.installed).map(([code, lang]) => ({\n lang,\n code,\n \"default\": code === currentLanguage,\n }));\n\n const available = Object.entries(moodleLangs.available).map(([code, lang]) => ({\n lang,\n code,\n \"default\": code === currentLanguage,\n }));\n\n return {\n installed,\n available,\n };\n};\n\n/**\n * Return moodle lang.\n *\n * @param {string} subtitleLang\n * @param {editor} editor\n * @returns {object|null}\n */\nexport const getMoodleLangObj = (subtitleLang, editor) => {\n const {available} = getMoodleLang(editor);\n\n if (available[subtitleLang]) {\n return {\n lang: subtitleLang,\n code: available[subtitleLang],\n };\n }\n\n return null;\n};\n\n/**\n * Get media data from the inserted media.\n *\n * @param {object} props\n * @returns {object}\n */\nexport const getEmbeddedMediaDetails = (props) => {\n const tracks = {\n subtitles: [],\n captions: [],\n descriptions: [],\n chapters: [],\n metadata: []\n };\n\n const mediaMetadata = props.root.querySelectorAll(Selectors.EMBED.elements.mediaMetadataTabPane);\n mediaMetadata.forEach(metaData => {\n const trackElements = metaData.querySelectorAll(Selectors.EMBED.elements.track);\n trackElements.forEach(track => {\n tracks[metaData.dataset.trackKind].push({\n src: track.querySelector(Selectors.EMBED.elements.url).value,\n srclang: track.querySelector(Selectors.EMBED.elements.trackLang).value,\n label: track.querySelector(Selectors.EMBED.elements.trackLabel).value,\n defaultTrack: track.querySelector(Selectors.EMBED.elements.trackDefault).checked,\n });\n });\n });\n\n const querySelector = (element) => props.root.querySelector(element);\n const mediaDataProps = {};\n mediaDataProps.media = {\n type: props.mediaType,\n sources: props.media,\n poster: props.media.poster ?? null,\n title: querySelector(Selectors.EMBED.elements.title).value,\n width: querySelector(Selectors.EMBED.elements.width).value,\n height: querySelector(Selectors.EMBED.elements.height).value,\n autoplay: querySelector(Selectors.EMBED.elements.mediaAutoplay).checked,\n loop: querySelector(Selectors.EMBED.elements.mediaLoop).checked,\n muted: querySelector(Selectors.EMBED.elements.mediaMute).checked,\n controls: querySelector(Selectors.EMBED.elements.mediaControl).checked,\n tracks,\n };\n mediaDataProps.link = false;\n return mediaDataProps;\n};\n\n/**\n * Check for video/audio attributes.\n *\n * @param {HTMLElement} elem\n * @param {string} attr Attribute name\n * @returns {boolean}\n */\nexport const hasAudioVideoAttr = (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"],"names":["props","mediaType","showDropzone","canShowDropZone","showFilePicker","canShowFilePicker","fileType","elementid","editor","id","showImageFilePicker","canShowImageFilePicker","bodyTemplate","Selectors","EMBED","template","body","insertMediaBody","footerTemplate","footer","insertMediaFooter","selector","type","selectedMedia","mediaElm","selection","getNode","nodeName","toLowerCase","querySelector","async","url","contextId","request","methodname","args","contextid","content","Ajax","call","contentType","fetch","method","headers","get","startsWith","e","parsedUrl","URL","pathName","pathname","extension","includes","split","pop","acceptedMediaTypes","hashTitle","hash","substring","mediaDetailsBody","mediaDetailsFooter","isLink","isVideo","showControl","isUpdating","isNewFileOrLinkUpload","newMediaLink","newFileUpload","subtitles","captions","descriptions","chapters","metadata","customsize","linkcustomsize","map","key","component","moodleLangs","currentLanguage","installed","Object","entries","_ref","code","lang","available","_ref2","subtitleLang","tracks","root","querySelectorAll","elements","mediaMetadataTabPane","forEach","metaData","track","dataset","trackKind","push","src","value","srclang","trackLang","label","trackLabel","defaultTrack","trackDefault","checked","element","mediaDataProps","media","sources","poster","title","width","height","autoplay","mediaAutoplay","loop","mediaLoop","muted","mediaMute","controls","mediaControl","link","elem","attr","hasAttribute","getAttribute"],"mappings":"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"}PK/<\W[ ZZ'amd/build/embed/embedpreview.min.js.mapnu[{"version":3,"file":"embedpreview.min.js","sources":["../../src/embed/embedpreview.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 preview and details class.\n *\n * This handles the embed file/url preview before embedding them into tiny editor.\n *\n * @module tiny_media/embed/embedpreview\n * @copyright 2024 Stevani Andolo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Selectors from '../selectors';\nimport {component} from '../common';\nimport {getString} from 'core/str';\nimport {\n sourceTypeChecked,\n getFileName,\n setPropertiesFromData,\n showElements,\n stopMediaLoading,\n hideElements,\n} from '../helpers';\nimport {EmbedHandler} from './embedhandler';\nimport {MediaBase} from '../mediabase';\nimport Notification from 'core/notification';\nimport EmbedModal from '../embedmodal';\nimport {\n getEmbeddedMediaDetails,\n insertMediaThumbnailTemplateContext,\n fetchPreview,\n} from './embedhelpers';\nimport {notifyFilterContentUpdated} from 'core_filters/events';\n\nexport class EmbedPreview extends MediaBase {\n\n // Selector type for \"EMBED\".\n selectorType = Selectors.EMBED.type;\n\n // Fixed aspect ratio used for external media providers.\n linkMediaAspectRatio = 1.78;\n\n constructor(data) {\n super();\n setPropertiesFromData(this, data); // Creates dynamic properties based on \"data\" param.\n }\n\n /**\n * Init the media details preview.\n */\n init = async() => {\n this.currentModal.setTitle(getString('mediadetails', component));\n sourceTypeChecked({\n fetchedTitle: this.fetchedMediaLinkTitle ?? null,\n source: this.originalUrl,\n root: this.root,\n urlSelector: Selectors.EMBED.elements.fromUrl,\n fileNameSelector: Selectors.EMBED.elements.fileNameLabel,\n });\n this.setMediaSourceAndPoster();\n this.registerMediaDetailsEventListeners(this.currentModal);\n };\n\n /**\n * Sets media source and thumbnail for the video.\n */\n setMediaSourceAndPoster = async() => {\n const box = this.root.querySelector(Selectors.EMBED.elements.previewBox);\n const previewArea = document.querySelector(Selectors.EMBED.elements.mediaPreviewContainer);\n previewArea.setAttribute('data-original-url', this.originalUrl);\n\n // Previewing existing media could be a link one.\n // Or, new media added using url input and mediaType is neither video or audio.\n if (this.mediaType === 'link' || (this.newMediaLink && !['video', 'audio'].includes(this.mediaType))) {\n previewArea.setAttribute('data-media-type', 'link');\n previewArea.innerHTML = await fetchPreview(this.originalUrl, this.contextId);\n notifyFilterContentUpdated(previewArea);\n } else if (this.mediaType === 'video') {\n const video = document.createElement('video');\n video.src = this.originalUrl;\n\n // Media url can be played using html video.\n video.addEventListener('loadedmetadata', () => {\n const videoHeight = video.videoHeight;\n const videoWidth = video.videoWidth;\n const widthProportion = (videoWidth - videoHeight);\n const isLandscape = widthProportion > 0;\n\n // Store dimensions of the raw video.\n this.mediaDimensions = {\n width: videoWidth,\n height: videoHeight,\n };\n\n // Set the media preview based on the media dimensions.\n if (isLandscape) {\n video.width = box.offsetWidth;\n } else {\n video.height = box.offsetHeight;\n }\n\n const height = this.root.querySelector(Selectors.EMBED.elements.height);\n const width = this.root.querySelector(Selectors.EMBED.elements.width);\n\n if (height.value === '' && width.value === '') {\n height.value = videoHeight;\n width.value = videoWidth;\n }\n\n // Size checking and adjustment.\n if (videoHeight === parseInt(height.value) && videoWidth === parseInt(width.value)) {\n this.currentWidth = this.mediaDimensions.width;\n this.currentHeight = this.mediaDimensions.height;\n this.sizeChecked('original');\n } else {\n this.currentWidth = parseInt(width.value);\n this.currentHeight = parseInt(height.value);\n this.sizeChecked('custom');\n }\n });\n\n video.controls = true;\n if (this.media.poster) {\n previewArea.setAttribute('data-media-poster', this.media.poster);\n if (!video.classList.contains('w-100')) {\n video.classList.add('w-100');\n }\n video.poster = this.media.poster;\n }\n video.load();\n\n previewArea.setAttribute('data-media-type', 'video');\n previewArea.innerHTML = video.outerHTML;\n notifyFilterContentUpdated(previewArea);\n } else if (this.mediaType === 'audio') {\n const audio = document.createElement('audio');\n audio.src = this.originalUrl;\n audio.controls = true;\n audio.load();\n\n previewArea.setAttribute('data-media-type', 'audio');\n previewArea.innerHTML = audio.outerHTML;\n notifyFilterContentUpdated(previewArea);\n } else {\n // Show warning notification.\n const urlWarningLabelEle = this.root.querySelector(Selectors.EMBED.elements.urlWarning);\n urlWarningLabelEle.innerHTML = await getString('medianotavailabledesc', component, this.originalUrl);\n showElements(Selectors.EMBED.elements.urlWarning, this.root);\n\n // Stop the spinner.\n stopMediaLoading(this.root, Selectors.EMBED.type);\n\n // Reset the upload form.\n (new EmbedHandler(this)).resetUploadForm();\n return;\n }\n\n // Stop the loader and display back the body template when the media is loaded.\n stopMediaLoading(this.root, Selectors.EMBED.type);\n showElements(Selectors.EMBED.elements.mediaDetailsBody, this.root);\n\n // Set the media name/title.\n this.root.querySelector(Selectors.EMBED.elements.title).value = this.setMediaTitle();\n };\n\n /**\n * Set media name/title.\n *\n * @returns {string}\n */\n setMediaTitle = () => {\n // Getting and setting up media title/name.\n let fileName = null;\n if (['video', 'audio'].includes(this.mediaType)) {\n fileName = getFileName(this.originalUrl); // Get original filename.\n } else if (this.fetchedMediaLinkTitle) {\n fileName = this.fetchedMediaLinkTitle;\n } else {\n fileName = this.originalUrl;\n }\n\n if (this.isUpdating) {\n if (!this.newMediaLink) {\n fileName = this.mediaTitle; // Title from the selected media.\n }\n }\n\n return decodeURI(fileName);\n };\n\n /**\n * Deletes the media after confirming with the user and loads the insert media page.\n */\n deleteMedia = () => {\n Notification.deleteCancelPromise(\n getString('deletemedia', component),\n getString('deletemediawarning', component),\n ).then(() => {\n // Reset media upload form.\n (new EmbedHandler(this)).resetUploadForm();\n\n // Delete any selected media mediaData.\n delete this.mediaData;\n return;\n }).catch(error => {\n window.console.log(error);\n });\n };\n\n /**\n * Delete embedded media thumbnail.\n */\n deleteEmbeddedThumbnail = () => {\n Notification.deleteCancelPromise(\n getString('deleteembeddedthumbnail', component),\n getString('deleteembeddedthumbnailwarning', component),\n ).then(async() => {\n if (this.mediaType === 'video') {\n const video = this.root.querySelector('video');\n if (video) {\n video.removeAttribute('poster');\n const preview = this.root.querySelector(Selectors.EMBED.elements.mediaPreviewContainer);\n preview.removeAttribute('data-media-poster');\n }\n }\n\n const deleteCustomThumbnail = this.root.querySelector(Selectors.EMBED.actions.deleteCustomThumbnail);\n deleteCustomThumbnail.remove();\n\n const uploadCustomThumbnail = this.root.querySelector(Selectors.EMBED.actions.uploadCustomThumbnail);\n uploadCustomThumbnail.textContent = await getString('uploadthumbnail', component);\n return;\n }).catch(error => {\n window.console.log(error);\n });\n };\n\n /**\n * Shows the insert thumbnail dialogue.\n */\n showUploadThumbnail = async() => {\n const uploadThumbnailModal = await EmbedModal.create({\n large: true,\n templateContext: {elementid: this.editor.getElement().id},\n });\n const root = uploadThumbnailModal.getRoot()[0];\n\n // Get selected media metadata.\n const mediaData = getEmbeddedMediaDetails(this);\n mediaData.isUpdating = this.isUpdating;\n\n const embedHandler = new EmbedHandler(this);\n embedHandler.loadInsertThumbnailTemplatePromise(\n insertMediaThumbnailTemplateContext(this), // Get template context for creating media thumbnail.\n {root, uploadThumbnailModal}, // Required root elements.\n await embedHandler.getMediaTemplateContext(mediaData) // Get current media data.\n );\n };\n\n /**\n * Only registers event listeners for new loaded elements in embed preview modal.\n */\n registerMediaDetailsEventListeners = async() => {\n // Handle the original size when selected.\n const sizeOriginalEle = this.root.querySelector(Selectors.EMBED.elements.sizeOriginal);\n if (sizeOriginalEle) {\n sizeOriginalEle.addEventListener('change', () => {\n this.sizeChecked('original');\n });\n }\n\n // Handle the custom size when selected.\n const sizeCustomEle = this.root.querySelector(Selectors.EMBED.elements.sizeCustom);\n if (sizeCustomEle) {\n sizeCustomEle.addEventListener('change', () => {\n this.sizeChecked('custom');\n });\n }\n\n const widthEle = this.root.querySelector(Selectors.EMBED.elements.width);\n const heightEle = this.root.querySelector(Selectors.EMBED.elements.height);\n\n // Handle the custom with size when inputted.\n if (widthEle) {\n widthEle.addEventListener('input', () => {\n if (this.mediaType === 'link') {\n // Let's apply the 16:9 aspect ratio if it's a link media type.\n heightEle.value = Math.round(widthEle.value / this.linkMediaAspectRatio);\n } else {\n // Avoid empty value.\n widthEle.value = widthEle.value === \"\" ? 0 : Number(widthEle.value);\n this.autoAdjustSize();\n }\n });\n }\n\n // Handle the custom height size when inputted.\n if (heightEle) {\n heightEle.addEventListener('input', () => {\n if (this.mediaType === 'link') {\n // Let's apply the 16:9 aspect ratio if it's a link media type.\n widthEle.value = Math.round(heightEle.value * this.linkMediaAspectRatio);\n } else {\n // Avoid empty value.\n heightEle.value = heightEle.value === \"\" ? 0 : Number(heightEle.value);\n this.autoAdjustSize(true);\n }\n });\n }\n\n // Handle media preview delete.\n const deleteMedia = this.root.querySelector(Selectors.EMBED.actions.deleteMedia);\n if (deleteMedia) {\n deleteMedia.addEventListener('click', (e) => {\n e.preventDefault();\n this.deleteMedia();\n });\n }\n\n // Show subtitles and captions settings.\n const showSubtitleCaption = this.root.querySelector(Selectors.EMBED.actions.showSubtitleCaption);\n if (showSubtitleCaption) {\n showSubtitleCaption.addEventListener('click', (e) => {\n e.preventDefault();\n hideElements([\n Selectors.EMBED.actions.showSubtitleCaption,\n Selectors.EMBED.actions.cancelMediaDetails,\n Selectors.EMBED.elements.mediaDetailsBody,\n ], this.root);\n showElements([\n Selectors.EMBED.actions.backToMediaDetails,\n Selectors.EMBED.elements.mediaSubtitleCaptionBody,\n ], this.root);\n });\n }\n\n // Back to media preview.\n const backToMediaDetails = this.root.querySelector(Selectors.EMBED.actions.backToMediaDetails);\n if (backToMediaDetails) {\n backToMediaDetails.addEventListener('click', () => {\n hideElements([\n Selectors.EMBED.actions.backToMediaDetails,\n Selectors.EMBED.elements.mediaSubtitleCaptionBody,\n ], this.root);\n showElements([\n Selectors.EMBED.actions.showSubtitleCaption,\n Selectors.EMBED.actions.cancelMediaDetails,\n Selectors.EMBED.elements.mediaDetailsBody,\n ], this.root);\n });\n }\n\n // Handles upload media thumbnail.\n const uploadCustomThumbnail = this.root.querySelector(Selectors.EMBED.actions.uploadCustomThumbnail);\n if (uploadCustomThumbnail) {\n uploadCustomThumbnail.addEventListener('click', () => {\n this.showUploadThumbnail();\n });\n }\n\n // Handles delete media thumbnail.\n const deleteCustomThumbnail = this.root.querySelector(Selectors.EMBED.actions.deleteCustomThumbnail);\n if (deleteCustomThumbnail) {\n deleteCustomThumbnail.addEventListener('click', () => {\n this.deleteEmbeddedThumbnail();\n });\n }\n\n // Handles language track selection.\n const langTracks = this.root.querySelectorAll(Selectors.EMBED.elements.trackLang);\n if (langTracks) {\n langTracks.forEach((dropdown) => {\n const defaultVal = dropdown.getAttribute('data-value');\n if (defaultVal) {\n Array.from(dropdown.options).some(option => {\n // Check if srclang in track is a language code like \"en\"\n // or language name like \"English\" prior to MDL-85159.\n if (option.dataset.languageCode === defaultVal || option.value === defaultVal) {\n option.selected = true;\n return true;\n }\n return false;\n });\n }\n });\n }\n };\n}\n"],"names":["EmbedPreview","MediaBase","constructor","data","Selectors","EMBED","type","async","currentModal","setTitle","component","fetchedTitle","this","fetchedMediaLinkTitle","source","originalUrl","root","urlSelector","elements","fromUrl","fileNameSelector","fileNameLabel","setMediaSourceAndPoster","registerMediaDetailsEventListeners","box","querySelector","previewBox","previewArea","document","mediaPreviewContainer","setAttribute","mediaType","newMediaLink","includes","innerHTML","contextId","video","createElement","src","addEventListener","videoHeight","videoWidth","isLandscape","mediaDimensions","width","height","offsetWidth","offsetHeight","value","parseInt","currentWidth","currentHeight","sizeChecked","controls","media","poster","classList","contains","add","load","outerHTML","urlWarning","EmbedHandler","resetUploadForm","audio","mediaDetailsBody","title","setMediaTitle","fileName","isUpdating","mediaTitle","decodeURI","deleteCancelPromise","then","mediaData","catch","error","window","console","log","removeAttribute","actions","deleteCustomThumbnail","remove","uploadCustomThumbnail","textContent","uploadThumbnailModal","EmbedModal","create","large","templateContext","elementid","editor","getElement","id","getRoot","embedHandler","loadInsertThumbnailTemplatePromise","getMediaTemplateContext","sizeOriginalEle","sizeOriginal","sizeCustomEle","sizeCustom","widthEle","heightEle","Math","round","linkMediaAspectRatio","Number","autoAdjustSize","deleteMedia","e","preventDefault","showSubtitleCaption","cancelMediaDetails","backToMediaDetails","mediaSubtitleCaptionBody","showUploadThumbnail","deleteEmbeddedThumbnail","langTracks","querySelectorAll","trackLang","forEach","dropdown","defaultVal","getAttribute","Array","from","options","some","option","dataset","languageCode","selected"],"mappings":"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"}PK/<\"%j%j%#amd/build/embed/embedpreview.min.jsnu[define("tiny_media/embed/embedpreview",["exports","../selectors","../common","core/str","../helpers","./embedhandler","../mediabase","core/notification","../embedmodal","./embedhelpers","core_filters/events"],(function(_exports,_selectors,_common,_str,_helpers,_embedhandler,_mediabase,_notification,_embedmodal,_embedhelpers,_events){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.EmbedPreview=void 0,_selectors=_interopRequireDefault(_selectors),_notification=_interopRequireDefault(_notification),_embedmodal=_interopRequireDefault(_embedmodal);class EmbedPreview extends _mediabase.MediaBase{constructor(data){super(),_defineProperty(this,"selectorType",_selectors.default.EMBED.type),_defineProperty(this,"linkMediaAspectRatio",1.78),_defineProperty(this,"init",(async()=>{var _this$fetchedMediaLin;this.currentModal.setTitle((0,_str.getString)("mediadetails",_common.component)),(0,_helpers.sourceTypeChecked)({fetchedTitle:null!==(_this$fetchedMediaLin=this.fetchedMediaLinkTitle)&&void 0!==_this$fetchedMediaLin?_this$fetchedMediaLin:null,source:this.originalUrl,root:this.root,urlSelector:_selectors.default.EMBED.elements.fromUrl,fileNameSelector:_selectors.default.EMBED.elements.fileNameLabel}),this.setMediaSourceAndPoster(),this.registerMediaDetailsEventListeners(this.currentModal)})),_defineProperty(this,"setMediaSourceAndPoster",(async()=>{const box=this.root.querySelector(_selectors.default.EMBED.elements.previewBox),previewArea=document.querySelector(_selectors.default.EMBED.elements.mediaPreviewContainer);if(previewArea.setAttribute("data-original-url",this.originalUrl),"link"===this.mediaType||this.newMediaLink&&!["video","audio"].includes(this.mediaType))previewArea.setAttribute("data-media-type","link"),previewArea.innerHTML=await(0,_embedhelpers.fetchPreview)(this.originalUrl,this.contextId),(0,_events.notifyFilterContentUpdated)(previewArea);else if("video"===this.mediaType){const video=document.createElement("video");video.src=this.originalUrl,video.addEventListener("loadedmetadata",(()=>{const videoHeight=video.videoHeight,videoWidth=video.videoWidth,isLandscape=videoWidth-videoHeight>0;this.mediaDimensions={width:videoWidth,height:videoHeight},isLandscape?video.width=box.offsetWidth:video.height=box.offsetHeight;const height=this.root.querySelector(_selectors.default.EMBED.elements.height),width=this.root.querySelector(_selectors.default.EMBED.elements.width);""===height.value&&""===width.value&&(height.value=videoHeight,width.value=videoWidth),videoHeight===parseInt(height.value)&&videoWidth===parseInt(width.value)?(this.currentWidth=this.mediaDimensions.width,this.currentHeight=this.mediaDimensions.height,this.sizeChecked("original")):(this.currentWidth=parseInt(width.value),this.currentHeight=parseInt(height.value),this.sizeChecked("custom"))})),video.controls=!0,this.media.poster&&(previewArea.setAttribute("data-media-poster",this.media.poster),video.classList.contains("w-100")||video.classList.add("w-100"),video.poster=this.media.poster),video.load(),previewArea.setAttribute("data-media-type","video"),previewArea.innerHTML=video.outerHTML,(0,_events.notifyFilterContentUpdated)(previewArea)}else{if("audio"!==this.mediaType){return this.root.querySelector(_selectors.default.EMBED.elements.urlWarning).innerHTML=await(0,_str.getString)("medianotavailabledesc",_common.component,this.originalUrl),(0,_helpers.showElements)(_selectors.default.EMBED.elements.urlWarning,this.root),(0,_helpers.stopMediaLoading)(this.root,_selectors.default.EMBED.type),void new _embedhandler.EmbedHandler(this).resetUploadForm()}{const audio=document.createElement("audio");audio.src=this.originalUrl,audio.controls=!0,audio.load(),previewArea.setAttribute("data-media-type","audio"),previewArea.innerHTML=audio.outerHTML,(0,_events.notifyFilterContentUpdated)(previewArea)}}(0,_helpers.stopMediaLoading)(this.root,_selectors.default.EMBED.type),(0,_helpers.showElements)(_selectors.default.EMBED.elements.mediaDetailsBody,this.root),this.root.querySelector(_selectors.default.EMBED.elements.title).value=this.setMediaTitle()})),_defineProperty(this,"setMediaTitle",(()=>{let fileName=null;return fileName=["video","audio"].includes(this.mediaType)?(0,_helpers.getFileName)(this.originalUrl):this.fetchedMediaLinkTitle?this.fetchedMediaLinkTitle:this.originalUrl,this.isUpdating&&(this.newMediaLink||(fileName=this.mediaTitle)),decodeURI(fileName)})),_defineProperty(this,"deleteMedia",(()=>{_notification.default.deleteCancelPromise((0,_str.getString)("deletemedia",_common.component),(0,_str.getString)("deletemediawarning",_common.component)).then((()=>{new _embedhandler.EmbedHandler(this).resetUploadForm(),delete this.mediaData})).catch((error=>{window.console.log(error)}))})),_defineProperty(this,"deleteEmbeddedThumbnail",(()=>{_notification.default.deleteCancelPromise((0,_str.getString)("deleteembeddedthumbnail",_common.component),(0,_str.getString)("deleteembeddedthumbnailwarning",_common.component)).then((async()=>{if("video"===this.mediaType){const video=this.root.querySelector("video");if(video){video.removeAttribute("poster");this.root.querySelector(_selectors.default.EMBED.elements.mediaPreviewContainer).removeAttribute("data-media-poster")}}this.root.querySelector(_selectors.default.EMBED.actions.deleteCustomThumbnail).remove();this.root.querySelector(_selectors.default.EMBED.actions.uploadCustomThumbnail).textContent=await(0,_str.getString)("uploadthumbnail",_common.component)})).catch((error=>{window.console.log(error)}))})),_defineProperty(this,"showUploadThumbnail",(async()=>{const uploadThumbnailModal=await _embedmodal.default.create({large:!0,templateContext:{elementid:this.editor.getElement().id}}),root=uploadThumbnailModal.getRoot()[0],mediaData=(0,_embedhelpers.getEmbeddedMediaDetails)(this);mediaData.isUpdating=this.isUpdating;const embedHandler=new _embedhandler.EmbedHandler(this);embedHandler.loadInsertThumbnailTemplatePromise((0,_embedhelpers.insertMediaThumbnailTemplateContext)(this),{root:root,uploadThumbnailModal:uploadThumbnailModal},await embedHandler.getMediaTemplateContext(mediaData))})),_defineProperty(this,"registerMediaDetailsEventListeners",(async()=>{const sizeOriginalEle=this.root.querySelector(_selectors.default.EMBED.elements.sizeOriginal);sizeOriginalEle&&sizeOriginalEle.addEventListener("change",(()=>{this.sizeChecked("original")}));const sizeCustomEle=this.root.querySelector(_selectors.default.EMBED.elements.sizeCustom);sizeCustomEle&&sizeCustomEle.addEventListener("change",(()=>{this.sizeChecked("custom")}));const widthEle=this.root.querySelector(_selectors.default.EMBED.elements.width),heightEle=this.root.querySelector(_selectors.default.EMBED.elements.height);widthEle&&widthEle.addEventListener("input",(()=>{"link"===this.mediaType?heightEle.value=Math.round(widthEle.value/this.linkMediaAspectRatio):(widthEle.value=""===widthEle.value?0:Number(widthEle.value),this.autoAdjustSize())})),heightEle&&heightEle.addEventListener("input",(()=>{"link"===this.mediaType?widthEle.value=Math.round(heightEle.value*this.linkMediaAspectRatio):(heightEle.value=""===heightEle.value?0:Number(heightEle.value),this.autoAdjustSize(!0))}));const deleteMedia=this.root.querySelector(_selectors.default.EMBED.actions.deleteMedia);deleteMedia&&deleteMedia.addEventListener("click",(e=>{e.preventDefault(),this.deleteMedia()}));const showSubtitleCaption=this.root.querySelector(_selectors.default.EMBED.actions.showSubtitleCaption);showSubtitleCaption&&showSubtitleCaption.addEventListener("click",(e=>{e.preventDefault(),(0,_helpers.hideElements)([_selectors.default.EMBED.actions.showSubtitleCaption,_selectors.default.EMBED.actions.cancelMediaDetails,_selectors.default.EMBED.elements.mediaDetailsBody],this.root),(0,_helpers.showElements)([_selectors.default.EMBED.actions.backToMediaDetails,_selectors.default.EMBED.elements.mediaSubtitleCaptionBody],this.root)}));const backToMediaDetails=this.root.querySelector(_selectors.default.EMBED.actions.backToMediaDetails);backToMediaDetails&&backToMediaDetails.addEventListener("click",(()=>{(0,_helpers.hideElements)([_selectors.default.EMBED.actions.backToMediaDetails,_selectors.default.EMBED.elements.mediaSubtitleCaptionBody],this.root),(0,_helpers.showElements)([_selectors.default.EMBED.actions.showSubtitleCaption,_selectors.default.EMBED.actions.cancelMediaDetails,_selectors.default.EMBED.elements.mediaDetailsBody],this.root)}));const uploadCustomThumbnail=this.root.querySelector(_selectors.default.EMBED.actions.uploadCustomThumbnail);uploadCustomThumbnail&&uploadCustomThumbnail.addEventListener("click",(()=>{this.showUploadThumbnail()}));const deleteCustomThumbnail=this.root.querySelector(_selectors.default.EMBED.actions.deleteCustomThumbnail);deleteCustomThumbnail&&deleteCustomThumbnail.addEventListener("click",(()=>{this.deleteEmbeddedThumbnail()}));const langTracks=this.root.querySelectorAll(_selectors.default.EMBED.elements.trackLang);langTracks&&langTracks.forEach((dropdown=>{const defaultVal=dropdown.getAttribute("data-value");defaultVal&&Array.from(dropdown.options).some((option=>(option.dataset.languageCode===defaultVal||option.value===defaultVal)&&(option.selected=!0,!0)))}))})),(0,_helpers.setPropertiesFromData)(this,data)}}_exports.EmbedPreview=EmbedPreview})); //# sourceMappingURL=embedpreview.min.js.mapPK/<\12 2 ,amd/build/embed/embedthumbnailpreview.min.jsnu[define("tiny_media/embed/embedthumbnailpreview",["exports","../selectors","core/str","../helpers","core/notification","./embedpreview","./embedhelpers","./embedhandler","../common"],(function(_exports,_selectors,_str,_helpers,_notification,_embedpreview,_embedhelpers,_embedhandler,_common){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.EmbedThumbnailPreview=void 0,_selectors=_interopRequireDefault(_selectors),_notification=_interopRequireDefault(_notification);_exports.EmbedThumbnailPreview=class{constructor(data){_defineProperty(this,"init",(mediaData=>{this.mediaData=mediaData,this.currentModal.uploadThumbnailModal.setTitle((0,_str.getString)("thumbnail",_common.component)),(0,_helpers.sourceTypeChecked)({source:this.media.poster,root:this.thumbnailModalRoot,fileNameSelector:_selectors.default.EMBED.elements.fileNameLabel}),this.setThumbnailSource(),this.registerMediaThumbnailEventListeners()})),_defineProperty(this,"setThumbnailSource",(()=>{const thumbnailPreview=this.thumbnailModalRoot.querySelector(_selectors.default.EMBED.elements.thumbnailPreview);thumbnailPreview.src=this.media.poster,thumbnailPreview.addEventListener("error",(async()=>{this.thumbnailModalRoot.querySelector(_selectors.default.EMBED.elements.urlWarning).innerHTML=await(0,_str.getString)("imageurlrequired",_common.component),(0,_helpers.showElements)(_selectors.default.EMBED.elements.urlWarning,this.thumbnailModalRoot),(0,_helpers.stopMediaLoading)(this.thumbnailModalRoot,_selectors.default.EMBED.type),new _embedhandler.EmbedHandler(this).resetUploadForm(!1)})),thumbnailPreview.addEventListener("load",(()=>{this.mediaData.media.poster=this.media.poster,this.media=this.mediaData.media,(0,_helpers.stopMediaLoading)(this.thumbnailModalRoot,_selectors.default.EMBED.type)}))})),_defineProperty(this,"deleteMedia",(()=>{_notification.default.deleteCancelPromise((0,_str.getString)("deletemediathumbnail",_common.component),(0,_str.getString)("deletemediathumbnailwarning",_common.component)).then((()=>{new _embedhandler.EmbedHandler(this).resetUploadForm(!1)})).catch((error=>{window.console.log(error)}))})),_defineProperty(this,"loadPreviewMediaThumbnail",(async()=>{new _embedhandler.EmbedHandler(this).loadMediaDetails(new _embedpreview.EmbedPreview(this),await(0,_embedhelpers.mediaDetailsTemplateContext)(this)).then((()=>{this.currentModal.uploadThumbnailModal.destroy();const currentModal=this.currentModal.insertMediaModal;this.currentModal=currentModal.insertMediaModal,delete this.mediaData})).catch((error=>{window.console.log(error)}))})),_defineProperty(this,"registerMediaThumbnailEventListeners",(()=>{const deleteMedia=this.thumbnailModalRoot.querySelector(_selectors.default.EMBED.actions.deleteThumbnail);deleteMedia&&deleteMedia.addEventListener("click",(e=>{e.preventDefault(),this.deleteMedia()}));const setPoster=this.thumbnailModalRoot.querySelector(_selectors.default.EMBED.actions.setPoster);setPoster&&setPoster.addEventListener("click",(()=>{this.loadPreviewMediaThumbnail()}))})),(0,_helpers.setPropertiesFromData)(this,data)}}})); //# sourceMappingURL=embedthumbnailpreview.min.js.mapPK/<\f66#amd/build/embed/embedhandler.min.jsnu[define("tiny_media/embed/embedhandler",["exports","../selectors","./embedinsert","../helpers","core/modal_events","editor_tiny/utils","./embedhelpers","core/templates","./embedthumbnailinsert"],(function(_exports,_selectors,_embedinsert,_helpers,ModalEvents,_utils,_embedhelpers,_templates,_embedthumbnailinsert){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.EmbedHandler=void 0,_selectors=_interopRequireDefault(_selectors),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),_templates=_interopRequireDefault(_templates);_exports.EmbedHandler=class{constructor(_data){var _this=this;_defineProperty(this,"loadTemplatePromise",(templateContext=>{templateContext.elementid=this.editor.id,templateContext.bodyTemplate=_selectors.default.EMBED.template.body.insertMediaBody,templateContext.footerTemplate=_selectors.default.EMBED.template.footer.insertMediaFooter,templateContext.selector=_selectors.default.EMBED.type,Promise.all([(0,_helpers.body)(templateContext,this.root),(0,_helpers.footer)(templateContext,this.root)]).then((()=>{new _embedinsert.EmbedInsert(this).init()})).catch((error=>{window.console.log(error)}))})),_defineProperty(this,"loadInsertThumbnailTemplatePromise",(async(templateContext,root,mediaData)=>{Promise.all([(0,_helpers.body)(templateContext,root.root),(0,_helpers.footer)(templateContext,root.root)]).then((()=>{this.currentModal.insertMediaModal||(this.currentModal.insertMediaModal=this.currentModal),root.uploadThumbnailModal&&(this.currentModal.uploadThumbnailModal=root.uploadThumbnailModal),this.thumbnailModalRoot=root.root,new _embedthumbnailinsert.EmbedThumbnailInsert(this).init(mediaData)})).catch((error=>{window.console.log(error)}))})),_defineProperty(this,"loadMediaDetails",(async(embedPreview,templateContext)=>{Promise.all([(0,_helpers.body)(templateContext,this.root),(0,_helpers.footer)(templateContext,this.root)]).then((()=>{embedPreview.init()})).catch((error=>{(0,_helpers.stopMediaLoading)(this.root,_selectors.default.EMBED.type),window.console.log(error)}))})),_defineProperty(this,"resetUploadForm",(function(){let isMediaInsert=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];isMediaInsert?(_this.newMediaLink=!1,_this.fetchedMediaLinkTitle=null,_this.resetCurrentMediaData(),_this.loadTemplatePromise((0,_embedhelpers.insertMediaTemplateContext)(_this))):_this.loadInsertThumbnailTemplatePromise((0,_embedhelpers.insertMediaThumbnailTemplateContext)(_this),{root:_this.thumbnailModalRoot},_this.mediaData)})),_defineProperty(this,"getMediaProperties",(()=>{var _media$getAttribute;const media=this.selectedMedia;if(!media)return null;const tracks={subtitles:[],captions:[],descriptions:[],chapters:[],metadata:[]},sources=[];media.querySelectorAll("track").forEach((track=>{tracks[track.getAttribute("kind")].push({src:track.getAttribute("src"),srclang:track.getAttribute("srclang"),label:track.getAttribute("label"),defaultTrack:(0,_embedhelpers.hasAudioVideoAttr)(track,"default")})})),media.querySelectorAll("source").forEach((source=>{sources.push(source.src)}));const title=null!==(_media$getAttribute=media.getAttribute("title"))&&void 0!==_media$getAttribute?_media$getAttribute:media.textContent;return{type:this.mediaType,sources:sources,poster:media.getAttribute("poster"),title:!!title&&title.trim(),width:media.getAttribute("width"),height:media.getAttribute("height"),autoplay:(0,_embedhelpers.hasAudioVideoAttr)(media,"autoplay"),loop:(0,_embedhelpers.hasAudioVideoAttr)(media,"loop"),muted:(0,_embedhelpers.hasAudioVideoAttr)(media,"muted"),controls:(0,_embedhelpers.hasAudioVideoAttr)(media,"controls"),tracks:tracks}})),_defineProperty(this,"getCurrentEmbedData",(()=>{const properties=this.getMediaProperties();if(!properties||this.newMediaLink)return{media:{}};const processedProperties={};return processedProperties.media=properties,processedProperties.link=!1,processedProperties})),_defineProperty(this,"getHelpStrings",(async()=>(this.helpStrings||(this.helpStrings=await(0,_embedhelpers.getHelpStrings)()),this.helpStrings))),_defineProperty(this,"getTemplateContext",(async data=>{const languages=(0,_embedhelpers.prepareMoodleLang)(this.editor),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,showFilePickerTrack:this.canShowFilePickerTrack,langsInstalled:languages.installed,langsAvailable:languages.available,media:!0,isUpdating:this.isUpdating},data,helpIcons)})),_defineProperty(this,"getMediaTemplateContext",(async function(){let data=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;return data?data.hasOwnProperty("isUpdating")?_this.isUpdating=data.isUpdating:_this.isUpdating=Object.keys(data).length>1:data=Object.assign({},_this.getCurrentEmbedData()),await _this.getTemplateContext(data)})),_defineProperty(this,"loadMediaPreview",(url=>{new _embedinsert.EmbedInsert(this).loadMediaPreview(url)})),_defineProperty(this,"resetCurrentMediaData",(()=>{this.media={},this.mediaType=null,this.selectedMedia=null})),_defineProperty(this,"getFilePickerTypeFromElement",(element=>element.closest(_selectors.default.EMBED.elements.posterSource)?"image":element.closest(_selectors.default.EMBED.elements.trackSource)?"subtitle":"media")),_defineProperty(this,"getTrackTypeFromTabPane",(tabPane=>tabPane.getAttribute("data-track-kind"))),_defineProperty(this,"clickHandler",(async 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.trackFilePickerCallback(params,element,fpType)}e.target.closest(_selectors.default.EMBED.actions.addUrl)&&((0,_helpers.startMediaLoading)(this.root,_selectors.default.EMBED.type),this.urlChanged());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(".tab-content").querySelectorAll(_selectors.default.EMBED.elements.trackDefault).forEach((select=>{select!==element&&getKind(element)===getKind(select)&&(select.checked=!1)}))}})),_defineProperty(this,"getMediaHTML",(form=>{this.mediaType=this.root.querySelector(_selectors.default.EMBED.elements.mediaPreviewContainer).dataset.mediaType;const tabContent=form.querySelector(".tab-content");return this["getMediaHTML"+this.mediaType[0].toUpperCase()+this.mediaType.substr(1)](tabContent)})),_defineProperty(this,"getMediaHTMLVideo",(tab=>{const details=document.querySelector(_selectors.default.EMBED.elements.mediaDetailsBody),context=this.getContextForMediaHTML(tab,details);context.width=details.querySelector(_selectors.default.EMBED.elements.width).value||!1,context.height=details.querySelector(_selectors.default.EMBED.elements.height).value||!1;const mediaPreviewContainer=details.querySelector(_selectors.default.EMBED.elements.mediaPreviewContainer);return context.poster=mediaPreviewContainer.dataset.mediaPoster||!1,context.sources?_templates.default.renderForPromise("tiny_media/embed/embed_media_video",context):""})),_defineProperty(this,"getMediaHTMLAudio",(tab=>{const details=document.querySelector(_selectors.default.EMBED.elements.mediaDetailsBody),context=this.getContextForMediaHTML(tab,details);return context.sources.length?_templates.default.renderForPromise("tiny_media/embed/embed_media_audio",context):""})),_defineProperty(this,"getContextForMediaHTML",((tab,details)=>{var _mediaPreviewContaine;const tracks=Array.from(tab.querySelectorAll(_selectors.default.EMBED.elements.track)).map((track=>{var _selectedLangTrack$da;const langTrack=track.querySelector(_selectors.default.EMBED.elements.trackLang),selectedLangTrack=langTrack.options[langTrack.selectedIndex];return{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||langTrack.value,srclang:null!==(_selectedLangTrack$da=selectedLangTrack.dataset.languageCode)&&void 0!==_selectedLangTrack$da&&_selectedLangTrack$da,defaultTrack:track.querySelector(_selectors.default.EMBED.elements.trackDefault).checked?"true":null}})).filter((track=>!!track.track)),mediaPreviewContainer=details.querySelector(_selectors.default.EMBED.elements.mediaPreviewContainer);let sources=null!==(_mediaPreviewContaine=mediaPreviewContainer.dataset.originalUrl)&&void 0!==_mediaPreviewContaine?_mediaPreviewContaine:null;this.alternativeSources&&(this.alternativeSources[0]=sources,sources=this.alternativeSources);const title=details.querySelector(_selectors.default.EMBED.elements.title).value;mediaPreviewContainer.removeAttribute("data-original-url");const templateContext={sources:sources,tracks:tracks,showControls:details.querySelector(_selectors.default.EMBED.elements.mediaControl).checked,autoplay:details.querySelector(_selectors.default.EMBED.elements.mediaAutoplay).checked,muted:details.querySelector(_selectors.default.EMBED.elements.mediaMute).checked,loop:details.querySelector(_selectors.default.EMBED.elements.mediaLoop).checked,title:""!==title&&title.trim()};return"link"===this.mediaType&&(templateContext.description=Array.isArray(sources)?sources[0]:sources),templateContext})),_defineProperty(this,"handleDialogueSubmission",(async(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))})),_defineProperty(this,"registerEventListeners",(async()=>{this.canShowFilePickerTrack&&this.root.addEventListener("click",this.clickHandler.bind(this)),this.root.addEventListener("input",(e=>{const urlEle=e.target.closest(_selectors.default.EMBED.elements.fromUrl);urlEle&&this.toggleUrlButton(urlEle,this.root)})),this.modalRoot.on(ModalEvents.hidden,(()=>{this.currentModal.destroy()})),this.modalRoot.on(ModalEvents.save,this.handleDialogueSubmission.bind(this))})),(0,_helpers.setPropertiesFromData)(this,_data)}urlChanged(){const url=this.root.querySelector(_selectors.default.EMBED.elements.fromUrl).value;url&&url!==this.currentUrl&&(this.mediaType=null,this.newMediaLink=!0,this.loadMediaPreview(url))}trackFilePickerCallback(params,element,fpType){if(""!==params.url){if(element.closest(".tab-pane")){if(element.closest(_selectors.default.EMBED.elements.source).querySelector(_selectors.default.EMBED.elements.url).value=params.url,"subtitle"===fpType){const subtitleLang=params.file.split(".vtt")[0].split("-").slice(-1)[0],langObj=(0,_embedhelpers.getMoodleLangObj)(subtitleLang,this.editor);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}}}else this.newFileUpload=!0,this.resetCurrentMediaData(),this.loadMediaPreview(params.url)}}addTrackComponent(element){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)}removeTrackComponent(element){element.closest(_selectors.default.EMBED.elements.track).remove()}toggleUrlButton(input,root){const url=input.value;root.querySelector(_selectors.default.EMBED.actions.addUrl).disabled=!(""!==url&&(0,_helpers.isValidUrl)(url))}getMediaHTMLLink(){var _document$querySelect;const mediaPreviewContainer=document.querySelector(_selectors.default.EMBED.elements.mediaPreviewContainer),context={name:null!==(_document$querySelect=document.querySelector(_selectors.default.EMBED.elements.title).value)&&void 0!==_document$querySelect?_document$querySelect:mediaPreviewContainer.dataset.originalUrl,url:mediaPreviewContainer.dataset.originalUrl||!1};return context.url?_templates.default.renderForPromise("tiny_media/embed/embed_media_link",context):""}}})); //# sourceMappingURL=embedhandler.min.js.mapPK/<\%W2{{+amd/build/embed/embedthumbnailinsert.min.jsnu[define("tiny_media/embed/embedthumbnailinsert",["exports","../selectors","core/dropzone","editor_tiny/uploader","core/prefetch","core/str","../common","../helpers","./embedthumbnailpreview","./embedhandler","editor_tiny/utils"],(function(_exports,_selectors,_dropzone,_uploader,_prefetch,_str,_common,_helpers,_embedthumbnailpreview,_embedhandler,_utils){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.EmbedThumbnailInsert=void 0,_selectors=_interopRequireDefault(_selectors),_dropzone=_interopRequireDefault(_dropzone),_uploader=_interopRequireDefault(_uploader),(0,_prefetch.prefetchStrings)(_common.component,["insertmediathumbnail","uploading","loadingembedthumbnail","addmediathumbnaildrop"]);_exports.EmbedThumbnailInsert=class{constructor(data){_defineProperty(this,"init",(async mediaData=>{this.mediaData=mediaData;const langStringKeys=["insertmediathumbnail","uploading","loadingembedthumbnail","addmediathumbnaildrop"],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.uploadThumbnailModal.setTitle(this.langStrings.insertmediathumbnail),this.canShowDropZone){const dropZoneEle=document.querySelector(_selectors.default.EMBED.elements.dropzoneContainer),dropZone=new _dropzone.default(dropZoneEle,this.acceptedImageTypes,(files=>{this.handleUploadedFile(files)}));dropZone.setLabel(this.langStrings.addmediathumbnaildrop),dropZone.init()}this.registerInsertMediaThumbnailEvents(this.thumbnailModalRoot)})),_defineProperty(this,"loadPreviewThumbnail",(url=>{this.media.poster=url;let templateContext={bodyTemplate:_selectors.default.EMBED.template.body.mediaThumbnailBody,footerTemplate:_selectors.default.EMBED.template.footer.mediaThumbnailFooter,selector:_selectors.default.EMBED.type};Promise.all([(0,_helpers.body)(templateContext,this.thumbnailModalRoot),(0,_helpers.footer)(templateContext,this.thumbnailModalRoot)]).then((()=>{new _embedthumbnailpreview.EmbedThumbnailPreview(this).init(this.mediaData)})).catch((error=>{window.console.log(error)}))})),_defineProperty(this,"filePickerCallback",(params=>{params.url&&this.loadPreviewThumbnail(params.url)})),_defineProperty(this,"updateLoaderIcon",(function(root,langStrings){let progress=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;const loaderIconState=root.querySelector(_selectors.default.EMBED.elements.loaderIconContainer+" div");loaderIconState.innerHTML=null!==progress?"".concat(langStrings.uploading," ").concat(Math.round(progress),"%"):langStrings.loadingembedthumbnail})),_defineProperty(this,"handleUploadedFile",(async files=>{try{(0,_helpers.startMediaLoading)(this.thumbnailModalRoot,_selectors.default.EMBED.type);const fileURL=await(0,_uploader.default)(this.editor,"image",files[0],files[0].name,(progress=>{this.updateLoaderIcon(this.thumbnailModalRoot,this.langStrings,progress)}));this.updateLoaderIcon(this.thumbnailModalRoot,this.langStrings),this.filePickerCallback({url:fileURL})}catch(error){this.thumbnailModalRoot.querySelector(_selectors.default.EMBED.elements.urlWarning).innerHTML=void 0!==error.error?error.error:error,(0,_helpers.showElements)(_selectors.default.EMBED.elements.urlWarning,this.thumbnailModalRoot),(0,_helpers.stopMediaLoading)(this.thumbnailModalRoot,_selectors.default.EMBED.type)}})),_defineProperty(this,"registerInsertMediaThumbnailEvents",(root=>{const urlEle=root.querySelector(_selectors.default.EMBED.elements.fromUrl);urlEle&&urlEle.addEventListener("input",(()=>{new _embedhandler.EmbedHandler(this).toggleUrlButton(urlEle,this.thumbnailModalRoot)}));const addUrlEle=root.querySelector(_selectors.default.EMBED.actions.addUrl);addUrlEle&&addUrlEle.addEventListener("click",(()=>{(0,_helpers.startMediaLoading)(this.thumbnailModalRoot,_selectors.default.EMBED.type),this.urlChanged()}));const imageBrowser=root.querySelector(_selectors.default.IMAGE.actions.imageBrowser);imageBrowser&&imageBrowser.addEventListener("click",(async e=>{e.preventDefault();const params=await(0,_utils.displayFilepicker)(this.editor,"image");this.filePickerCallback(params)}))})),(0,_helpers.setPropertiesFromData)(this,data)}urlChanged(){const url=this.thumbnailModalRoot.querySelector(_selectors.default.EMBED.elements.fromUrl).value;url&&url!==this.currentUrl&&this.loadPreviewThumbnail(url)}}})); //# sourceMappingURL=embedthumbnailinsert.min.js.mapPK/<\ܼF"amd/build/embed/embedinsert.min.jsnu[define("tiny_media/embed/embedinsert",["exports","core/prefetch","core/str","../common","../helpers","../selectors","core/dropzone","editor_tiny/uploader","./embedhandler","./embedhelpers","./embedpreview"],(function(_exports,_prefetch,_str,_common,_helpers,_selectors,_dropzone,_uploader,_embedhandler,_embedhelpers,_embedpreview){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.EmbedInsert=void 0,_selectors=_interopRequireDefault(_selectors),_dropzone=_interopRequireDefault(_dropzone),_uploader=_interopRequireDefault(_uploader),(0,_prefetch.prefetchStrings)(_common.component,["insertmedia","addmediafilesdrop","uploading","loadingmedia"]);_exports.EmbedInsert=class{constructor(data){_defineProperty(this,"init",(async()=>{const langStringKeys=["insertmedia","addmediafilesdrop","uploading","loadingmedia"],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.insertmedia),this.canShowDropZone&&!this.mediaType){const dropZoneEle=document.querySelector(_selectors.default.EMBED.elements.dropzoneContainer),dropZone=new _dropzone.default(dropZoneEle,this.acceptedMediaTypes,(files=>{this.handleUploadedFile(files)}));dropZone.setLabel(this.langStrings.addmediafilesdrop),dropZone.init()}})),_defineProperty(this,"loadMediaPreview",(async url=>{const parsedUrl=new URL(url);if(this.fetchedMediaLinkTitle=await(0,_embedhelpers.getMediaTitle)(url,this),this.originalUrl=parsedUrl.hash?parsedUrl.toString().split("#")[0]:parsedUrl.toString(),this.newMediaLink){if(this.filteredContent=await(0,_embedhelpers.fetchPreview)(this.originalUrl,this.contextId),!this.mediaType)if(this.filteredContent&&""!==this.filteredContent.trim())this.mediaType="link";else if(this.fetchedMediaLinkTitle){const audioRegex=/]*>.*<\/audio>/i;/]*>.*<\/video>/i.test(this.filteredContent)?this.mediaType="video":audioRegex.test(this.filteredContent)&&(this.mediaType="audio")}this.processMediaPreview()}else{var _this$mediaType;null!==(_this$mediaType=this.mediaType)&&void 0!==_this$mediaType||(this.mediaType=await(0,_embedhelpers.checkMediaType)(url)),this.mediaType||(this.filteredContent=await(0,_embedhelpers.fetchPreview)(this.originalUrl,this.contextId),this.filteredContent&&""!==this.filteredContent.trim()&&(this.mediaType="link")),this.processMediaPreview()}})),_defineProperty(this,"processMediaPreview",(async()=>{(0,_helpers.setPropertiesFromData)(this,await new _embedhandler.EmbedHandler(this).getMediaTemplateContext());const templateContext=await(0,_embedhelpers.mediaDetailsTemplateContext)(this);this.isUpdating&&!this.newMediaLink&&(this.mediaTitle=templateContext.media.title),new _embedhandler.EmbedHandler(this).loadMediaDetails(new _embedpreview.EmbedPreview(this),templateContext)})),_defineProperty(this,"updateLoaderIcon",(function(root,langStrings){let progress=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;const loaderIconState=root.querySelector(_selectors.default.EMBED.elements.loaderIconContainer+" div");loaderIconState.innerHTML=null!==progress?"".concat(langStrings.uploading," ").concat(Math.round(progress),"%"):langStrings.loadingmedia})),_defineProperty(this,"filePickerCallback",(params=>{params.url&&(this.mediaType&&(this.mediaType=null),this.newFileUpload=!0,this.loadMediaPreview(params.url))})),_defineProperty(this,"handleUploadedFile",(async files=>{try{(0,_helpers.startMediaLoading)(this.root,_selectors.default.EMBED.type);const fileURL=await(0,_uploader.default)(this.editor,"media",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.EMBED.elements.urlWarning).innerHTML=void 0!==error.error?error.error:error,(0,_helpers.showElements)(_selectors.default.EMBED.elements.urlWarning,this.root),(0,_helpers.stopMediaLoading)(this.root,_selectors.default.EMBED.type)}})),(0,_helpers.setPropertiesFromData)(this,data)}}})); //# sourceMappingURL=embedinsert.min.js.mapPK/<\v9#amd/build/embed/embedhelpers.min.jsnu[define("tiny_media/embed/embedhelpers",["exports","../selectors","core/str","../common","editor_tiny/options","core/ajax","../helpers"],(function(_exports,_selectors,_str,_common,_options,_ajax,_helpers){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Tiny media plugin embed helpers. * * This provides easy access to any classes without instantiating a new object. * * @module tiny_media/embed/embedhelpers * @copyright 2024 Stevani Andolo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.prepareMoodleLang=_exports.mediaDetailsTemplateContext=_exports.insertMediaThumbnailTemplateContext=_exports.insertMediaTemplateContext=_exports.hasAudioVideoAttr=_exports.getSelectedMediaElement=_exports.getMoodleLangObj=_exports.getMediaTitle=_exports.getHelpStrings=_exports.getEmbeddedMediaDetails=_exports.fetchPreview=_exports.checkMediaType=void 0,_selectors=_interopRequireDefault(_selectors),_ajax=_interopRequireDefault(_ajax);_exports.insertMediaTemplateContext=props=>({mediaType:props.mediaType,showDropzone:props.canShowDropZone,showFilePicker:props.canShowFilePicker,fileType:"audio/video"});_exports.insertMediaThumbnailTemplateContext=props=>({elementid:props.editor.id,showDropzone:props.canShowDropZone,showImageFilePicker:props.canShowImageFilePicker,bodyTemplate:_selectors.default.EMBED.template.body.insertMediaBody,footerTemplate:_selectors.default.EMBED.template.footer.insertMediaFooter,fileType:"image",selector:_selectors.default.EMBED.type});_exports.getSelectedMediaElement=editor=>{let mediaType=null,selectedMedia=null;const mediaElm=editor.selection.getNode();return mediaElm?"video"===mediaElm.nodeName.toLowerCase()||"audio"===mediaElm.nodeName.toLowerCase()?(mediaType=mediaElm.nodeName.toLowerCase(),selectedMedia=mediaElm):mediaElm.querySelector("video")?(mediaType="video",selectedMedia=mediaElm.querySelector("video")):mediaElm.querySelector("audio")?(mediaType="audio",selectedMedia=mediaElm.querySelector("audio")):"a"===mediaElm.nodeName.toLowerCase()&&(selectedMedia=mediaElm):(mediaType=null,selectedMedia=null),[mediaType,selectedMedia]};_exports.fetchPreview=async(url,contextId)=>{const request={methodname:"tiny_media_preview",args:{contextid:contextId,content:url}};return(await _ajax.default.call([request])[0]).content};_exports.checkMediaType=async url=>{try{const contentType=(await fetch(url,{method:"HEAD"})).headers.get("Content-type");return contentType?contentType.startsWith("video/")?"video":contentType.startsWith("audio/")?"audio":null:null}catch(e){return null}};_exports.getMediaTitle=async(url,props)=>{const parsedUrl=new URL(url),pathName=parsedUrl.pathname,extension=pathName.includes(".")?pathName.split(".").pop():null;if(extension&&props.acceptedMediaTypes.includes(".".concat(extension)))return(0,_helpers.getFileName)(url);const hashTitle=parsedUrl.hash?parsedUrl.hash.substring(1):null;return hashTitle||null};_exports.mediaDetailsTemplateContext=async props=>({...{bodyTemplate:_selectors.default.EMBED.template.body.mediaDetailsBody,footerTemplate:_selectors.default.EMBED.template.footer.mediaDetailsFooter,isLink:"link"===props.mediaType,isVideo:"video"===props.mediaType,showControl:"video"===props.mediaType||"audio"===props.mediaType,isUpdating:props.isUpdating,isNewFileOrLinkUpload:props.newMediaLink||props.newFileUpload,selector:_selectors.default.EMBED.type},...props});_exports.getHelpStrings=async()=>{const[subtitles,captions,descriptions,chapters,metadata,customsize,linkcustomsize]=await(0,_str.getStrings)(["subtitles_help","captions_help","descriptions_help","chapters_help","metadata_help","customsize_help","linkcustomsize_help"].map((key=>({key:key,component:_common.component}))));return{subtitles:subtitles,captions:captions,descriptions:descriptions,chapters:chapters,metadata:metadata,customsize:customsize,linkcustomsize:linkcustomsize}};_exports.prepareMoodleLang=editor=>{const moodleLangs=(0,_options.getMoodleLang)(editor),currentLanguage=(0,_options.getCurrentLanguage)(editor);return{installed:Object.entries(moodleLangs.installed).map((_ref=>{let[code,lang]=_ref;return{lang:lang,code:code,default:code===currentLanguage}})),available:Object.entries(moodleLangs.available).map((_ref2=>{let[code,lang]=_ref2;return{lang:lang,code:code,default:code===currentLanguage}}))}};_exports.getMoodleLangObj=(subtitleLang,editor)=>{const{available:available}=(0,_options.getMoodleLang)(editor);return available[subtitleLang]?{lang:subtitleLang,code:available[subtitleLang]}:null};_exports.getEmbeddedMediaDetails=props=>{var _props$media$poster;const tracks={subtitles:[],captions:[],descriptions:[],chapters:[],metadata:[]};props.root.querySelectorAll(_selectors.default.EMBED.elements.mediaMetadataTabPane).forEach((metaData=>{metaData.querySelectorAll(_selectors.default.EMBED.elements.track).forEach((track=>{tracks[metaData.dataset.trackKind].push({src:track.querySelector(_selectors.default.EMBED.elements.url).value,srclang:track.querySelector(_selectors.default.EMBED.elements.trackLang).value,label:track.querySelector(_selectors.default.EMBED.elements.trackLabel).value,defaultTrack:track.querySelector(_selectors.default.EMBED.elements.trackDefault).checked})}))}));const querySelector=element=>props.root.querySelector(element),mediaDataProps={};return mediaDataProps.media={type:props.mediaType,sources:props.media,poster:null!==(_props$media$poster=props.media.poster)&&void 0!==_props$media$poster?_props$media$poster:null,title:querySelector(_selectors.default.EMBED.elements.title).value,width:querySelector(_selectors.default.EMBED.elements.width).value,height:querySelector(_selectors.default.EMBED.elements.height).value,autoplay:querySelector(_selectors.default.EMBED.elements.mediaAutoplay).checked,loop:querySelector(_selectors.default.EMBED.elements.mediaLoop).checked,muted:querySelector(_selectors.default.EMBED.elements.mediaMute).checked,controls:querySelector(_selectors.default.EMBED.elements.mediaControl).checked,tracks:tracks},mediaDataProps.link=!1,mediaDataProps};_exports.hasAudioVideoAttr=(elem,attr)=>elem.hasAttribute(attr)&&(elem.getAttribute(attr)||""===elem.getAttribute(attr))})); //# sourceMappingURL=embedhelpers.min.js.mapPK/<\6T||'amd/build/embed/embedhandler.min.js.mapnu[{"version":3,"file":"embedhandler.min.js","sources":["../../src/embed/embedhandler.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 handler class.\n *\n * This handles anything that embed requires like:\n * - Calling the media preview in embedPreview.\n * - Loading the embed insert.\n * - Getting selected media data.\n * - Handles url and repository uploads.\n * - Reset embed insert when embed preview is deleted.\n * - Handles media embedding into tiny and etc.\n *\n * @module tiny_media/embed/embedhandler\n * @copyright 2024 Stevani Andolo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Selectors from \"../selectors\";\nimport {EmbedInsert} from './embedinsert';\nimport {\n body,\n footer,\n setPropertiesFromData,\n isValidUrl,\n stopMediaLoading,\n startMediaLoading,\n} from '../helpers';\nimport * as ModalEvents from 'core/modal_events';\nimport {displayFilepicker} from 'editor_tiny/utils';\nimport {\n insertMediaTemplateContext,\n getHelpStrings,\n prepareMoodleLang,\n getMoodleLangObj,\n hasAudioVideoAttr,\n insertMediaThumbnailTemplateContext,\n} from \"./embedhelpers\";\nimport Templates from 'core/templates';\nimport {EmbedThumbnailInsert} from './embedthumbnailinsert';\n\nexport class EmbedHandler {\n\n constructor(data) {\n setPropertiesFromData(this, data); // Creates dynamic properties based on \"data\" param.\n }\n\n /**\n * Load the media insert dialogue.\n *\n * @param {object} templateContext Object template context\n */\n loadTemplatePromise = (templateContext) => {\n templateContext.elementid = this.editor.id;\n templateContext.bodyTemplate = Selectors.EMBED.template.body.insertMediaBody;\n templateContext.footerTemplate = Selectors.EMBED.template.footer.insertMediaFooter;\n templateContext.selector = Selectors.EMBED.type;\n\n Promise.all([body(templateContext, this.root), footer(templateContext, this.root)])\n .then(() => {\n (new EmbedInsert(this)).init();\n return;\n })\n .catch(error => {\n window.console.log(error);\n });\n };\n\n /**\n * Load the media thumbnail insert dialogue.\n *\n * @param {object} templateContext Object template context\n * @param {HTMLElement} root\n * @param {object} mediaData\n */\n loadInsertThumbnailTemplatePromise = async(templateContext, root, mediaData) => {\n Promise.all([body(templateContext, root.root), footer(templateContext, root.root)])\n .then(() => {\n if (!this.currentModal.insertMediaModal) {\n this.currentModal.insertMediaModal = this.currentModal;\n }\n\n if (root.uploadThumbnailModal) {\n this.currentModal.uploadThumbnailModal = root.uploadThumbnailModal;\n }\n\n this.thumbnailModalRoot = root.root;\n (new EmbedThumbnailInsert(this)).init(mediaData);\n return;\n })\n .catch(error => {\n window.console.log(error);\n });\n };\n\n /**\n * Loads the media preview dialogue.\n *\n * @param {object} embedPreview Object of embedPreview\n * @param {object} templateContext Object of template context\n */\n loadMediaDetails = async(embedPreview, templateContext) => {\n Promise.all([body(templateContext, this.root), footer(templateContext, this.root)])\n .then(() => {\n embedPreview.init();\n return;\n })\n .catch(error => {\n stopMediaLoading(this.root, Selectors.EMBED.type);\n window.console.log(error);\n });\n };\n\n /**\n * Reset the media/thumbnail insert modal form.\n *\n * @param {boolean} isMediaInsert Is current state media insert or thumbnail insert?\n */\n resetUploadForm = (isMediaInsert = true) => {\n if (isMediaInsert) {\n this.newMediaLink = false;\n this.fetchedMediaLinkTitle = null;\n this.resetCurrentMediaData();\n this.loadTemplatePromise(insertMediaTemplateContext(this));\n } else {\n this.loadInsertThumbnailTemplatePromise(\n insertMediaThumbnailTemplateContext(this), // Get template context for creating media thumbnail.\n {root: this.thumbnailModalRoot}, // Required root elements.\n this.mediaData // Get current media data.\n );\n }\n };\n\n /**\n * Get selected media data.\n *\n * @returns {null|object}\n */\n getMediaProperties = () => {\n const media = this.selectedMedia;\n if (!media) {\n return null;\n }\n\n const tracks = {\n subtitles: [],\n captions: [],\n descriptions: [],\n chapters: [],\n metadata: []\n };\n const sources = [];\n\n media.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: hasAudioVideoAttr(track, 'default')\n });\n });\n\n media.querySelectorAll('source').forEach((source) => {\n sources.push(source.src);\n });\n const title = media.getAttribute('title') ?? media.textContent;\n\n return {\n type: this.mediaType,\n sources,\n poster: media.getAttribute('poster'),\n title: title ? title.trim() : false,\n width: media.getAttribute('width'),\n height: media.getAttribute('height'),\n autoplay: hasAudioVideoAttr(media, 'autoplay'),\n loop: hasAudioVideoAttr(media, 'loop'),\n muted: hasAudioVideoAttr(media, 'muted'),\n controls: hasAudioVideoAttr(media, 'controls'),\n tracks,\n };\n };\n\n /**\n * Get selected media data.\n *\n * @returns {object}\n */\n getCurrentEmbedData = () => {\n const properties = this.getMediaProperties();\n if (!properties || this.newMediaLink) {\n return {media: {}};\n }\n\n const processedProperties = {};\n processedProperties.media = properties;\n processedProperties.link = false;\n\n return processedProperties;\n };\n\n /**\n * Get help strings for media subtitles and captions.\n *\n * @returns {null|object}\n */\n getHelpStrings = async() => {\n if (!this.helpStrings) {\n this.helpStrings = await getHelpStrings();\n }\n\n return this.helpStrings;\n };\n\n /**\n * Set template context for insert media dialogue.\n *\n * @param {object} data Object of media data\n * @returns {object}\n */\n getTemplateContext = async(data) => {\n const languages = prepareMoodleLang(this.editor);\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 showFilePickerTrack: this.canShowFilePickerTrack,\n langsInstalled: languages.installed,\n langsAvailable: languages.available,\n media: true,\n isUpdating: this.isUpdating,\n }, data, helpIcons);\n };\n\n /**\n * Set and get media template context.\n *\n * @param {null|object} data Null or object of media data\n * @returns {Promise} A promise that resolves template context.\n */\n getMediaTemplateContext = async(data = null) => {\n if (!data) {\n data = Object.assign({}, this.getCurrentEmbedData());\n } else {\n if (data.hasOwnProperty('isUpdating')) {\n this.isUpdating = data.isUpdating;\n } else {\n this.isUpdating = Object.keys(data).length > 1;\n }\n }\n return await this.getTemplateContext(data);\n };\n\n /**\n * Handles changes in the media URL input field and loads a preview of the media if the URL has changed.\n */\n urlChanged() {\n const url = this.root.querySelector(Selectors.EMBED.elements.fromUrl).value;\n if (url && url !== this.currentUrl) {\n // Set to null on new url change.\n this.mediaType = null;\n\n // Flag as new media link insert.\n this.newMediaLink = true;\n this.loadMediaPreview(url);\n }\n }\n\n /**\n * Load the media preview dialogue.\n *\n * @param {string} url String of media url\n */\n loadMediaPreview = (url) => {\n (new EmbedInsert(this)).loadMediaPreview(url);\n };\n\n /**\n * Callback for file picker that previews the media or add the captions and subtitles.\n *\n * @param {object} params Object of media url and etc\n * @param {html} element Selected element.\n * @param {string} fpType Caption type.\n */\n trackFilePickerCallback(params, element, fpType) {\n if (params.url !== '') {\n const tabPane = element.closest('.tab-pane');\n if (tabPane) {\n element.closest(Selectors.EMBED.elements.source).querySelector(Selectors.EMBED.elements.url).value = params.url;\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 = getMoodleLangObj(subtitleLang, this.editor);\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 } else {\n // Flag as new file upload.\n this.newFileUpload = true;\n this.resetCurrentMediaData();\n this.loadMediaPreview(params.url);\n }\n }\n }\n\n /**\n * Reset current media data.\n */\n resetCurrentMediaData = () => {\n // Reset the value of the following props.\n this.media = {};\n this.mediaType = null;\n this.selectedMedia = null;\n };\n\n /**\n * Add new html track element.\n *\n * @param {html} element\n */\n addTrackComponent(element) {\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 trackElement.parentNode.insertBefore(clone, trackElement.nextSibling);\n }\n\n /**\n * Remove added html track element.\n *\n * @param {html} element\n */\n removeTrackComponent(element) {\n const sourceElement = element.closest(Selectors.EMBED.elements.track);\n sourceElement.remove();\n }\n\n /**\n * Get picker type based on the selected element.\n *\n * @param {html} element Selected element\n * @returns {string}\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 /**\n * Get captions/subtitles type.\n *\n * @param {html} tabPane\n * @returns {string}\n */\n getTrackTypeFromTabPane = (tabPane) => {\n return tabPane.getAttribute('data-track-kind');\n };\n\n /**\n * Handle click events.\n *\n * @param {html} e Selected element\n */\n clickHandler = async(e) => {\n const element = e.target;\n\n // Handle repository browsing.\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.trackFilePickerCallback(params, element, fpType);\n }\n\n // Handles add media url.\n const addUrlEle = e.target.closest(Selectors.EMBED.actions.addUrl);\n if (addUrlEle) {\n startMediaLoading(this.root, Selectors.EMBED.type);\n this.urlChanged();\n }\n\n // Handles adding tracks.\n const addComponentTrackAction = element.closest(Selectors.EMBED.elements.track + ' .addcomponent');\n if (addComponentTrackAction) {\n e.preventDefault();\n this.addTrackComponent(element);\n }\n\n // Handles removing added tracks.\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('.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 /**\n * Enables or disables the URL-related buttons in the footer based on the current URL and input value.\n *\n * @param {html} input Url input field\n * @param {object} root\n */\n toggleUrlButton(input, root) {\n const url = input.value;\n const addUrl = root.querySelector(Selectors.EMBED.actions.addUrl);\n addUrl.disabled = !(url !== \"\" && isValidUrl(url));\n }\n\n /**\n * Get media html to be inserted or updated into tiny.\n *\n * @param {html} form Selected element\n * @returns {string} String of html\n */\n getMediaHTML = (form) => {\n this.mediaType = this.root.querySelector(Selectors.EMBED.elements.mediaPreviewContainer).dataset.mediaType;\n const tabContent = form.querySelector('.tab-content');\n const callback = 'getMediaHTML' + this.mediaType[0].toUpperCase() + this.mediaType.substr(1);\n return this[callback](tabContent);\n };\n\n /**\n * Get media as link.\n *\n * @returns {string} String of html.\n */\n getMediaHTMLLink() {\n const mediaPreviewContainer = document.querySelector(Selectors.EMBED.elements.mediaPreviewContainer);\n const context = {\n name: document.querySelector(Selectors.EMBED.elements.title).value ?? mediaPreviewContainer.dataset.originalUrl,\n url: mediaPreviewContainer.dataset.originalUrl || false\n };\n\n return context.url ? Templates.renderForPromise('tiny_media/embed/embed_media_link', context) : '';\n }\n\n /**\n * Get media as video.\n *\n * @param {html} tab Selected element\n * @returns {string} String of html.\n */\n getMediaHTMLVideo = (tab) => {\n const details = document.querySelector(Selectors.EMBED.elements.mediaDetailsBody);\n const context = this.getContextForMediaHTML(tab, details);\n context.width = details.querySelector(Selectors.EMBED.elements.width).value || false;\n context.height = details.querySelector(Selectors.EMBED.elements.height).value || false;\n\n const mediaPreviewContainer = details.querySelector(Selectors.EMBED.elements.mediaPreviewContainer);\n context.poster = mediaPreviewContainer.dataset.mediaPoster || false;\n return context.sources ? Templates.renderForPromise('tiny_media/embed/embed_media_video', context) : '';\n };\n\n /**\n * Get media as audio.\n *\n * @param {html} tab Selected element\n * @returns {string} String of html.\n */\n getMediaHTMLAudio = (tab) => {\n const details = document.querySelector(Selectors.EMBED.elements.mediaDetailsBody);\n const context = this.getContextForMediaHTML(tab, details);\n return context.sources.length ? Templates.renderForPromise('tiny_media/embed/embed_media_audio', context) : '';\n };\n\n /**\n * Get previewed media data.\n *\n * @param {html} tab Selected element\n * @param {html} details Selected element\n * @returns {object}\n */\n getContextForMediaHTML = (tab, details) => {\n const tracks = Array.from(tab.querySelectorAll(Selectors.EMBED.elements.track)).map(track => {\n const langTrack = track.querySelector(Selectors.EMBED.elements.trackLang);\n const selectedLangTrack = langTrack.options[langTrack.selectedIndex];\n\n return {\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 || langTrack.value,\n srclang: selectedLangTrack.dataset.languageCode ?? false,\n defaultTrack: track.querySelector(Selectors.EMBED.elements.trackDefault).checked ? \"true\" : null\n };\n }).filter((track) => !!track.track);\n\n const mediaPreviewContainer = details.querySelector(Selectors.EMBED.elements.mediaPreviewContainer);\n let sources = mediaPreviewContainer.dataset.originalUrl ?? null;\n\n // Let's check if media has more than one sources.\n if (this.alternativeSources) {\n // Always update the first item in this.alternativeSources to the new one.\n this.alternativeSources[0] = sources;\n // Override the sources to have all the updated sources.\n sources = this.alternativeSources;\n }\n\n const title = details.querySelector(Selectors.EMBED.elements.title).value;\n // Remove data-original-url attribute once it's extracted.\n mediaPreviewContainer.removeAttribute('data-original-url');\n\n const templateContext = {\n sources,\n tracks,\n showControls: details.querySelector(Selectors.EMBED.elements.mediaControl).checked,\n autoplay: details.querySelector(Selectors.EMBED.elements.mediaAutoplay).checked,\n muted: details.querySelector(Selectors.EMBED.elements.mediaMute).checked,\n loop: details.querySelector(Selectors.EMBED.elements.mediaLoop).checked,\n title: title !== '' ? title.trim() : false,\n };\n\n // Add description prop to templateContext if media type is \"link\".\n if (this.mediaType === 'link') {\n // Let's form an alternative title.\n templateContext.description = Array.isArray(sources) ? sources[0] : sources;\n }\n\n return templateContext;\n };\n\n /**\n * Handle the insert/update media in tiny editor.\n *\n * @param {event} event\n * @param {object} modal Object of current modal\n */\n handleDialogueSubmission = async(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 /**\n * Register insert media modal elements' events.\n */\n registerEventListeners = async() => {\n // Handles click events for insert media modal.\n if (this.canShowFilePickerTrack) {\n this.root.addEventListener('click', this.clickHandler.bind(this));\n }\n\n // Handles media adding using url input.\n this.root.addEventListener('input', (e) => {\n const urlEle = e.target.closest(Selectors.EMBED.elements.fromUrl);\n if (urlEle) {\n this.toggleUrlButton(urlEle, this.root);\n }\n });\n\n // Destroy created modal when it's closed.\n this.modalRoot.on(ModalEvents.hidden, () => {\n this.currentModal.destroy();\n });\n\n // Handles media insert to editor.\n this.modalRoot.on(ModalEvents.save, this.handleDialogueSubmission.bind(this));\n };\n}\n"],"names":["constructor","data","templateContext","elementid","this","editor","id","bodyTemplate","Selectors","EMBED","template","body","insertMediaBody","footerTemplate","footer","insertMediaFooter","selector","type","Promise","all","root","then","EmbedInsert","init","catch","error","window","console","log","async","mediaData","currentModal","insertMediaModal","uploadThumbnailModal","thumbnailModalRoot","EmbedThumbnailInsert","embedPreview","isMediaInsert","_this","newMediaLink","fetchedMediaLinkTitle","resetCurrentMediaData","loadTemplatePromise","loadInsertThumbnailTemplatePromise","media","selectedMedia","tracks","subtitles","captions","descriptions","chapters","metadata","sources","querySelectorAll","forEach","track","getAttribute","push","src","srclang","label","defaultTrack","source","title","textContent","mediaType","poster","trim","width","height","autoplay","loop","muted","controls","properties","getMediaProperties","processedProperties","link","helpStrings","languages","helpIcons","Array","from","Object","entries","getHelpStrings","_ref","key","text","toLowerCase","assign","getElement","showFilePickerTrack","canShowFilePickerTrack","langsInstalled","installed","langsAvailable","available","isUpdating","hasOwnProperty","keys","length","getCurrentEmbedData","getTemplateContext","url","loadMediaPreview","element","closest","elements","posterSource","trackSource","tabPane","e","target","actions","mediaBrowser","preventDefault","fpType","getFilePickerTypeFromElement","params","trackFilePickerCallback","addUrl","urlChanged","addTrackComponent","removeTrackComponent","trackDefaultAction","trackDefault","checked","getKind","el","getTrackTypeFromTabPane","parentElement","select","form","querySelector","mediaPreviewContainer","dataset","tabContent","toUpperCase","substr","tab","details","document","mediaDetailsBody","context","getContextForMediaHTML","value","mediaPoster","Templates","renderForPromise","map","langTrack","trackLang","selectedLangTrack","options","selectedIndex","kind","trackLabel","languageCode","filter","originalUrl","alternativeSources","removeAttribute","showControls","mediaControl","mediaAutoplay","mediaMute","mediaLoop","description","isArray","event","modal","html","getMediaHTML","getRoot","outerHTML","insertContent","addEventListener","clickHandler","bind","urlEle","fromUrl","toggleUrlButton","modalRoot","on","ModalEvents","hidden","destroy","save","handleDialogueSubmission","currentUrl","subtitleLang","file","split","slice","langObj","lang","code","newFileUpload","trackElement","clone","cloneNode","classList","remove","add","parentNode","insertBefore","nextSibling","input","disabled","getMediaHTMLLink","name"],"mappings":"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"}PK/<\X&0amd/build/embed/embedthumbnailpreview.min.js.mapnu[{"version":3,"file":"embedthumbnailpreview.min.js","sources":["../../src/embed/embedthumbnailpreview.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 thumbnail preview class.\n *\n * This handles:\n * - Embed thumbnail preview.\n * - Delete thumbnail preview.\n * - Update the embed preview details with new thumbnail.\n *\n * @module tiny_media/embed/mediathumbnail\n * @copyright 2024 Stevani Andolo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Selectors from '../selectors';\nimport {getString} from 'core/str';\nimport {\n sourceTypeChecked,\n setPropertiesFromData,\n showElements,\n stopMediaLoading,\n} from '../helpers';\nimport Notification from 'core/notification';\nimport {EmbedPreview} from './embedpreview';\nimport {mediaDetailsTemplateContext} from './embedhelpers';\nimport {EmbedHandler} from './embedhandler';\nimport {component} from '../common';\n\nexport class EmbedThumbnailPreview {\n\n constructor(data) {\n setPropertiesFromData(this, data); // Creates dynamic properties based on \"data\" param.\n }\n\n /**\n * Init the media thumbnail preview.\n *\n * @param {object} mediaData Object of selected media data\n */\n init = (mediaData) => {\n this.mediaData = mediaData;\n this.currentModal.uploadThumbnailModal.setTitle(getString('thumbnail', component));\n sourceTypeChecked({\n source: this.media.poster,\n root: this.thumbnailModalRoot,\n fileNameSelector: Selectors.EMBED.elements.fileNameLabel,\n });\n this.setThumbnailSource();\n this.registerMediaThumbnailEventListeners();\n };\n\n /**\n * Sets media thumbnail source.\n */\n setThumbnailSource = () => {\n const thumbnailPreview = this.thumbnailModalRoot.querySelector(Selectors.EMBED.elements.thumbnailPreview);\n thumbnailPreview.src = this.media.poster;\n\n thumbnailPreview.addEventListener('error', async() => {\n // Show warning notification.\n const urlWarningLabelEle = this.thumbnailModalRoot.querySelector(Selectors.EMBED.elements.urlWarning);\n urlWarningLabelEle.innerHTML = await getString('imageurlrequired', component);\n showElements(Selectors.EMBED.elements.urlWarning, this.thumbnailModalRoot);\n\n // Stop the spinner.\n stopMediaLoading(this.thumbnailModalRoot, Selectors.EMBED.type);\n\n // Reset the upload form.\n (new EmbedHandler(this)).resetUploadForm(false);\n });\n\n thumbnailPreview.addEventListener('load', () => {\n this.mediaData.media.poster = this.media.poster;\n this.media = this.mediaData.media;\n stopMediaLoading(this.thumbnailModalRoot, Selectors.EMBED.type);\n });\n };\n\n /**\n * Deletes the media after confirming with the user and loads the insert media page.\n */\n deleteMedia = () => {\n Notification.deleteCancelPromise(\n getString('deletemediathumbnail', component),\n getString('deletemediathumbnailwarning', component)\n ).then(() => {\n (new EmbedHandler(this)).resetUploadForm(false);\n return;\n }).catch(error => {\n window.console.log(error);\n });\n };\n\n /**\n * Loads and displays a media preview with thumbnail.\n */\n loadPreviewMediaThumbnail = async() => {\n (new EmbedHandler(this)).loadMediaDetails(new EmbedPreview(this), await mediaDetailsTemplateContext(this))\n .then(() => {\n // Close the thumbnail upload modal once media details have been loaded.\n this.currentModal.uploadThumbnailModal.destroy();\n const currentModal = this.currentModal.insertMediaModal;\n this.currentModal = currentModal.insertMediaModal;\n delete this.mediaData;\n return;\n }).catch(error => {\n window.console.log(error);\n });\n };\n\n /**\n * Only registers event listeners for new loaded elements in mediaThumbnail.\n */\n registerMediaThumbnailEventListeners = () => {\n // Handles delete thumbnail.\n const deleteMedia = this.thumbnailModalRoot.querySelector(Selectors.EMBED.actions.deleteThumbnail);\n if (deleteMedia) {\n deleteMedia.addEventListener('click', (e) => {\n e.preventDefault();\n this.deleteMedia();\n });\n }\n\n // Handles setting the media poster.\n const setPoster = this.thumbnailModalRoot.querySelector(Selectors.EMBED.actions.setPoster);\n if (setPoster) {\n setPoster.addEventListener('click', () => {\n this.loadPreviewMediaThumbnail();\n });\n }\n };\n}\n"],"names":["constructor","data","mediaData","currentModal","uploadThumbnailModal","setTitle","component","source","this","media","poster","root","thumbnailModalRoot","fileNameSelector","Selectors","EMBED","elements","fileNameLabel","setThumbnailSource","registerMediaThumbnailEventListeners","thumbnailPreview","querySelector","src","addEventListener","async","urlWarning","innerHTML","type","EmbedHandler","resetUploadForm","deleteCancelPromise","then","catch","error","window","console","log","loadMediaDetails","EmbedPreview","destroy","insertMediaModal","deleteMedia","actions","deleteThumbnail","e","preventDefault","setPoster","loadPreviewMediaThumbnail"],"mappings":"8vBA4CIA,YAAYC,mCASJC,iBACCA,UAAYA,eACZC,aAAaC,qBAAqBC,UAAS,kBAAU,YAAaC,mDACrD,CACdC,OAAQC,KAAKC,MAAMC,OACnBC,KAAMH,KAAKI,mBACXC,iBAAkBC,mBAAUC,MAAMC,SAASC,qBAE1CC,0BACAC,qFAMY,WACXC,iBAAmBZ,KAAKI,mBAAmBS,cAAcP,mBAAUC,MAAMC,SAASI,kBACxFA,iBAAiBE,IAAMd,KAAKC,MAAMC,OAElCU,iBAAiBG,iBAAiB,SAASC,UAEZhB,KAAKI,mBAAmBS,cAAcP,mBAAUC,MAAMC,SAASS,YACvEC,gBAAkB,kBAAU,mBAAoBpB,6CACtDQ,mBAAUC,MAAMC,SAASS,WAAYjB,KAAKI,kDAGtCJ,KAAKI,mBAAoBE,mBAAUC,MAAMY,UAGrDC,2BAAapB,MAAOqB,iBAAgB,MAG7CT,iBAAiBG,iBAAiB,QAAQ,UACjCrB,UAAUO,MAAMC,OAASF,KAAKC,MAAMC,YACpCD,MAAQD,KAAKN,UAAUO,oCACXD,KAAKI,mBAAoBE,mBAAUC,MAAMY,gDAOpD,2BACGG,qBACT,kBAAU,uBAAwBxB,oBAClC,kBAAU,8BAA+BA,oBAC3CyB,MAAK,SACEH,2BAAapB,MAAOqB,iBAAgB,MAE1CG,OAAMC,QACLC,OAAOC,QAAQC,IAAIH,+DAOCT,cACnBI,2BAAapB,MAAO6B,iBAAiB,IAAIC,2BAAa9B,YAAa,6CAA4BA,OACnGuB,MAAK,UAEG5B,aAAaC,qBAAqBmC,gBACjCpC,aAAeK,KAAKL,aAAaqC,sBAClCrC,aAAeA,aAAaqC,wBAC1BhC,KAAKN,aAEb8B,OAAMC,QACLC,OAAOC,QAAQC,IAAIH,0EAOY,WAE7BQ,YAAcjC,KAAKI,mBAAmBS,cAAcP,mBAAUC,MAAM2B,QAAQC,iBAC9EF,aACAA,YAAYlB,iBAAiB,SAAUqB,IACnCA,EAAEC,sBACGJ,uBAKPK,UAAYtC,KAAKI,mBAAmBS,cAAcP,mBAAUC,MAAM2B,QAAQI,WAC5EA,WACAA,UAAUvB,iBAAiB,SAAS,UAC3BwB,qEAhGSvC,KAAMP"}PK/<\ s amd/build/options.min.js.mapnu[{"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_media/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');\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 });\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"],"names":["dataName","pluginName","permissionsName","editor","registerOption","options","register","processor","image","filepicker","getPermissions","get","embed"],"mappings":";;;;;;;;MA0BMA,UAAW,gCAAoBC,mBAAY,QAC3CC,iBAAkB,gCAAoBD,mBAAY,iCAO/BE,eACfC,eAAiBD,OAAOE,QAAQC,SAEtCF,eAAeF,gBAAiB,CAC5BK,UAAW,iBACA,CACPC,MAAO,CACHC,YAAY,MAKxBL,eAAeJ,SAAU,CACrBO,UAAW,kBAUNG,eAAkBP,QAAWA,OAAOE,QAAQM,IAAIT,qFAQzBC,QAAWO,eAAeP,QAAQK,mCAQlCL,QAAWO,eAAeP,QAAQS,uBAQ9CT,QAAWA,OAAOE,QAAQM,IAAIX"}PK/<\00amd/build/mediabase.min.jsnu[define("tiny_media/mediabase",["exports","./helpers","./selectors"],(function(_exports,_helpers,_selectors){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.MediaBase=void 0,_selectors=(obj=_selectors)&&obj.__esModule?obj:{default:obj};_exports.MediaBase=class{constructor(){var _this=this;_defineProperty(this,"sizeChecked",(async option=>{const widthInput=this.root.querySelector(_selectors.default[this.selectorType].elements.width),heightInput=this.root.querySelector(_selectors.default[this.selectorType].elements.height);if("original"===option)this.sizeOriginalChecked(),widthInput.value=this.mediaDimensions.width,heightInput.value=this.mediaDimensions.height;else if("custom"===option&&(this.sizeCustomChecked(),widthInput.value=this.currentWidth,heightInput.value=this.currentHeight,this.selectorType===_selectors.default.IMAGE.type&&this.currentWidth===this.mediaDimensions.width&&this.currentHeight===this.mediaDimensions.height)){this.root.querySelector(_selectors.default[this.selectorType].elements.constrain).checked=!0}this.autoAdjustSize()})),_defineProperty(this,"autoAdjustSize",(function(){let forceHeight=arguments.length>0&&void 0!==arguments[0]&&arguments[0];if(!_this.mediaDimensions)return;const widthField=_this.root.querySelector(_selectors.default[_this.selectorType].elements.width),heightField=_this.root.querySelector(_selectors.default[_this.selectorType].elements.height),normalizeFieldData=fieldData=>(fieldData.isPercentageValue=(0,_helpers.isPercentageValue)(fieldData.field.value),fieldData.isPercentageValue?(fieldData.percentValue=parseInt(fieldData.field.value,10),fieldData.pixelSize=_this.mediaDimensions[fieldData.type]/100*fieldData.percentValue):(fieldData.pixelSize=parseInt(fieldData.field.value,10),fieldData.percentValue=fieldData.pixelSize/_this.mediaDimensions[fieldData.type]*100),fieldData),getKeyField=()=>{const currentValue=forceHeight?{field:heightField,type:"height"}:{field:widthField,type:"width"};return""===currentValue.field.value&&(currentValue.field.value=_this.mediaDimensions[currentValue.type]),normalizeFieldData(currentValue)},getRelativeField=()=>normalizeFieldData(forceHeight?{field:widthField,type:"width"}:{field:heightField,type:"height"}),constrainField=_this.root.querySelector(_selectors.default[_this.selectorType].elements.constrain);if(constrainField&&constrainField.checked||"video"===_this.mediaType){const keyField=getKeyField(),relativeField=getRelativeField();keyField.isPercentageValue?(relativeField.field.value=keyField.field.value,relativeField.percentValue=keyField.percentValue):(relativeField.pixelSize=Math.round(keyField.pixelSize/_this.mediaDimensions[keyField.type]*_this.mediaDimensions[relativeField.type]),relativeField.field.value=relativeField.pixelSize)}_this.selectorType===_selectors.default.IMAGE.type&&(_this.currentWidth=Number(widthField.value)!==_this.mediaDimensions.width?widthField.value:_this.currentWidth,_this.currentHeight=Number(heightField.value)!==_this.mediaDimensions.height?heightField.value:_this.currentHeight)}))}sizeOriginalChecked(){this.root.querySelector(_selectors.default[this.selectorType].elements.sizeOriginal).checked=!0,this.root.querySelector(_selectors.default[this.selectorType].elements.sizeCustom).checked=!1,(0,_helpers.hideElements)(_selectors.default[this.selectorType].elements.properties,this.root)}sizeCustomChecked(){this.root.querySelector(_selectors.default[this.selectorType].elements.sizeOriginal).checked=!1,this.root.querySelector(_selectors.default[this.selectorType].elements.sizeCustom).checked=!0,(0,_helpers.showElements)(_selectors.default[this.selectorType].elements.properties,this.root)}}})); //# sourceMappingURL=mediabase.min.js.mapPK/<\fXkkamd/build/commands.min.jsnu[define("tiny_media/commands",["exports","core/str","./common","./image","./embed","./manager","editor_tiny/utils","./selectors"],(function(_exports,_str,_common,_image,_embed,_manager,_utils,_selectors){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Tiny Media commands. * * @module tiny_media/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,_image=_interopRequireDefault(_image),_embed=_interopRequireDefault(_embed),_manager=_interopRequireDefault(_manager),_selectors=_interopRequireDefault(_selectors);const isImage=node=>"img"===node.nodeName.toLowerCase(),isVideo=node=>"video"===node.nodeName.toLowerCase()||"audio"===node.nodeName.toLowerCase();_exports.getSetup=async()=>{const[imageButtonText,mediaButtonText,mediaManagerButtonText]=await(0,_str.getStrings)(["imagebuttontitle","mediabuttontitle","mediamanagerbuttontitle"].map((key=>({key:key,component:_common.component})))),[mediaManagerButtonImage]=await Promise.all([(0,_utils.getButtonImage)("filemanager",_common.component)]);return editor=>{((editor,imageButtonText)=>{const handleImageAction=()=>{new _image.default(editor).displayDialogue()};editor.ui.registry.addToggleButton(_common.imageButtonName,{icon:"image",tooltip:imageButtonText,onAction:handleImageAction,onSetup:api=>editor.selection.selectorChangedWithUnbind("img:not([data-mce-object]):not([data-mce-placeholder]),figure.image",api.setActive).unbind}),editor.ui.registry.addMenuItem(_common.imageButtonName,{icon:"image",text:imageButtonText,onAction:handleImageAction}),editor.ui.registry.addContextToolbar(_common.imageButtonName,{predicate:isImage,items:_common.imageButtonName,position:"node",scope:"node"}),editor.ui.registry.addContextMenu(_common.imageButtonName,{update:isImage}),editor.on("SetContent",(function(){editor.getBody().querySelectorAll("img:not(.".concat(_selectors.default.IMAGE.styles.responsive,")")).forEach((img=>img.classList.add("".concat(_selectors.default.IMAGE.styles.responsive))))}))})(editor,imageButtonText),((editor,videoButtonText)=>{const handleEmbedAction=()=>{new _embed.default(editor).displayDialogue()};editor.ui.registry.addToggleButton(_common.videoButtonName,{icon:"embed",tooltip:videoButtonText,onAction:handleEmbedAction,onSetup:api=>editor.selection.selectorChangedWithUnbind("video:not([data-mce-object]):not([data-mce-placeholder]),audio:not([data-mce-object]):not([data-mce-placeholder]),a:not([data-mce-object]):not([data-mce-placeholder])[class=external-media-provider]",api.setActive).unbind}),editor.ui.registry.addMenuItem(_common.videoButtonName,{icon:"embed",text:videoButtonText,onAction:handleEmbedAction}),editor.ui.registry.addContextMenu(_common.videoButtonName,{update:isVideo}),editor.ui.registry.addContextToolbar(_common.videoButtonName,{predicate:isVideo,items:_common.videoButtonName,position:"node",scope:"node"})})(editor,mediaButtonText),((editor,mediaManagerButtonText,mediaManagerButtonImage)=>{const handleMediaManager=()=>{new _manager.default(editor).displayDialogue()};editor.ui.registry.addIcon("filemanager",mediaManagerButtonImage.html),editor.ui.registry.addButton(_common.mediaManagerButtonName,{icon:"filemanager",tooltip:mediaManagerButtonText,onAction:()=>{handleMediaManager()}}),editor.ui.registry.addMenuItem(_common.mediaManagerButtonName,{icon:"filemanager",text:mediaManagerButtonText,onAction:()=>{handleMediaManager()}})})(editor,mediaManagerButtonText,mediaManagerButtonImage)}}})); //# sourceMappingURL=commands.min.js.mapPK/<\3ZUamd/build/selectors.min.jsnu[define("tiny_media/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={IMAGE:{actions:{submit:".tiny_image_urlentrysubmit",imageBrowser:".openimagebrowser",addUrl:".tiny_image_addurl",deleteImage:".tiny_image_deleteicon"},elements:{form:"form.tiny_image_form",alignSettings:".tiny_image_button",alt:".tiny_image_altentry",altWarning:".tiny_image_altwarning",height:".tiny_image_heightentry",width:".tiny_image_widthentry",url:".tiny_image_urlentry",urlWarning:".tiny_image_urlwarning",size:".tiny_image_size",presentation:".tiny_image_presentation",constrain:".tiny_image_constrain",customStyle:".tiny_image_customstyle",preview:".tiny_image_preview",previewBox:".tiny_image_preview_box",loaderIcon:".tiny_image_loader",loaderIconContainer:".tiny_image_loader_container",insertImage:".tiny_image_insert_image",modalFooter:".modal-footer",dropzoneContainer:".tiny_image_dropzone_container",fileInput:"#tiny_image_fileinput",fileNameLabel:".tiny_image_filename",sizeOriginal:".tiny_image_sizeoriginal",sizeCustom:".tiny_image_sizecustom",properties:".tiny_image_properties",bodyTemplate:".tiny_image_body_template",footerTemplate:".tiny_image_footer_template"},styles:{responsive:"img-fluid"},template:{body:{insertImageBody:"tiny_media/insert_image_modal_insert",insertImageDetailsBody:"tiny_media/insert_image_modal_details"},footer:{insertImageFooter:"tiny_media/insert_image_modal_insert_footer",insertImageDetailsFooter:"tiny_media/insert_image_modal_details_footer"}},type:"IMAGE"},EMBED:{actions:{mediaBrowser:".openmediabrowser",addUrl:".tiny_media_add_url",deleteMedia:".tiny_media_delete_icon",showSubtitleCaption:'[data-action="show-subtitle-caption"]',backToMediaDetails:'[data-action="back-to-media-details"]',uploadCustomThumbnail:".upload-custom-thumbnail",deleteCustomThumbnail:".delete-custom-thumbnail",deleteThumbnail:".delete_tiny_thumbnail",setPoster:'[data-action="set-poster"]',mediaLinkAsAudio:".link_as_audio",mediaLinkAsVideo:".link_as_video",cancelMediaDetails:'[data-action="cancel"]'},elements:{source:".tiny_media_source",track:".tiny_media_track",posterSource:".tiny_media_poster_source",title:".tiny_media_title_entry",url:".tiny_media_source_url_entry",width:".tiny_media_width_entry",height:".tiny_media_height_entry",trackSource:".tiny_media_track_source",trackLabel:".tiny_media_track_label_entry",trackLang:".tiny_media_track_lang_entry",trackDefault:".tiny_media_track_default",mediaControl:".tiny_media_controls",mediaAutoplay:".tiny_media_autoplay",mediaMute:".tiny_media_mute",mediaLoop:".tiny_media_loop",bodyTemplate:".tiny_media_body_template",footerTemplate:".tiny_media_footer_template",dropzoneContainer:".tiny_media_dropzone_container",fromUrl:".tiny_media_from_url_entry",urlWarning:".tiny_media_url_warning",loaderIcon:".tiny_media_loader",loaderIconContainer:".tiny_media_loader_container",insertMedia:".tiny_media_insert_media",modalFooter:".modal-footer",fileNameLabel:".tiny_media_filename",previewBox:".tiny_media_preview_box",sizeOriginal:".tiny_media_sizeoriginal",sizeCustom:".tiny_media_sizecustom",properties:".tiny_media_properties",mediaDetailsBody:"#tiny-media-details-body",mediaSubtitleCaptionBody:"#tiny-media-subtitle-caption-body",mediaMetadataTabPane:".tab-pane-media-metadata",videoThumbnail:".video-thumbnail",mediaSizeProperties:".media-size-properties",thumbnailPreview:".tiny_media_thumbnail_preview",mediaPreviewContainer:"#media-filter-preview-container"},template:{body:{insertMediaBody:"tiny_media/embed/body/insert_media_body",mediaDetailsBody:"tiny_media/embed/body/media_details_body",mediaThumbnailBody:"tiny_media/embed/body/media_thumbnail_body"},footer:{insertMediaFooter:"tiny_media/embed/footer/insert_media_footer",mediaDetailsFooter:"tiny_media/embed/footer/media_details_footer",mediaThumbnailFooter:"tiny_media/embed/footer/media_thumbnail_footer"}},type:"EMBED",externalMediaProvider:"external-media-provider"}},_exports.default})); //# sourceMappingURL=selectors.min.js.mapPK/<\з,,amd/build/imagedetails.min.jsnu[define("tiny_media/imagedetails",["exports","core/config","core/modal_events","core/notification","core/pending","./selectors","core/templates","core/str","tiny_media/imageinsert","./mediabase","./helpers"],(function(_exports,_config,_modal_events,_notification,_pending,_selectors,_templates,_str,_imageinsert,_mediabase,_helpers){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);class ImageDetails extends _mediabase.MediaBase{constructor(root,editor,currentModal,canShowFilePicker,canShowDropZone,currentUrl,_image){super(),_defineProperty(this,"DEFAULTS",{WIDTH:160,HEIGHT:160}),_defineProperty(this,"selectorType",_selectors.default.IMAGE.type),_defineProperty(this,"mediaDimensions",null),_defineProperty(this,"init",(function(){this.currentModal.setTitle((0,_str.getString)("imagedetails","tiny_media")),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,bodyTemplate:_selectors.default.IMAGE.template.body.insertImageBody,footerTemplate:_selectors.default.IMAGE.template.footer.insertImageFooter,selector:_selectors.default.IMAGE.type};Promise.all([(0,_helpers.body)(templateContext,this.root),(0,_helpers.footer)(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.mediaDimensions.width),element.value))(this.root.querySelector(_selectors.default.IMAGE.elements.width)),currentHeight=(element=>(""===element.value&&(element.value=this.mediaDimensions.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,_helpers.isPercentageValue)(currentWidth)&&(0,_helpers.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.mediaDimensions.width===currentWidth&&this.mediaDimensions.height===currentHeight?(this.currentWidth=this.mediaDimensions.width,this.currentHeight=this.mediaDimensions.height,this.sizeChecked("original")):(this.currentWidth=currentWidth,this.currentHeight=currentHeight,this.sizeChecked("custom"))})(Number(currentWidth),Number(currentHeight))}async presentationChanged(){const presentation=this.root.querySelector(_selectors.default.IMAGE.elements.presentation);this.root.querySelector(_selectors.default.IMAGE.elements.alt).disabled=presentation.checked,await this.handleKeyupCharacterCount()}imageTypeChecked(){const isExternalUrl=!1===new RegExp("".concat(_config.default.wwwroot)).test(this.currentUrl);if((0,_helpers.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,_helpers.showElements)(_selectors.default.IMAGE.elements.urlWarning,this.root):(0,_helpers.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,_helpers.showElements)(_selectors.default.IMAGE.elements.altWarning,this.root):(0,_helpers.hideElements)(_selectors.default.IMAGE.elements.altWarning,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_media: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,_helpers.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,_helpers.isPercentageValue)(height)&&isNaN(parseInt(height,10)))return this.root.querySelector(_selectors.default.IMAGE.elements.height).focus(),void pendingPromise.resolve();_templates.default.render("tiny_media/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_media"),(0,_str.getString)("deleteimagewarning","tiny_media")).then((()=>{(0,_helpers.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",(async e=>{e.target.closest(_selectors.default.IMAGE.elements.presentation)&&await 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",(async e=>{if(e.target.nodeType===Node.ELEMENT_NODE){e.target.closest(_selectors.default.IMAGE.elements.presentation)&&await this.presentationChanged()}}),!0),this.root.addEventListener("keyup",(async e=>{e.target.closest(_selectors.default.IMAGE.elements.alt)&&await 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))}))}async handleKeyupCharacterCount(){const altField=this.root.querySelector(_selectors.default.IMAGE.elements.alt),alt=altField.value;this.root.querySelector("#currentcount").innerHTML=alt.length;const maxLength=altField.getAttribute("maxlength"),maxLengthFeedback=document.getElementById("maxlength_feedback");if(alt.length>=maxLength){maxLengthFeedback.textContent=await(0,_str.getString)("maxlengthreached","core",maxLength);const suffix=this.toggleMaxlengthFeedbackSuffix?"":".";maxLengthFeedback.textContent+=suffix,this.toggleMaxlengthFeedbackSuffix=!this.toggleMaxlengthFeedbackSuffix,setTimeout((()=>{maxLengthFeedback.textContent=""}),4e3)}}}_exports.ImageDetails=ImageDetails})); //# sourceMappingURL=imagedetails.min.js.mapPK/<\U2amd/build/embed.min.js.mapnu[{"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_media/embed\n * @copyright 2022 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport EmbedModal from './embedmodal';\nimport {getEmbedPermissions} from './options';\nimport {getFilePicker, getContextId} from 'editor_tiny/options';\nimport {EmbedHandler} from './embed/embedhandler';\nimport {insertMediaTemplateContext, getSelectedMediaElement} from './embed/embedhelpers';\nimport {EmbedInsert} from './embed/embedinsert';\nimport {startMediaLoading} from './helpers';\nimport Selectors from \"./selectors\";\n\nexport default class MediaEmbed {\n editor = null;\n canShowFilePicker = false;\n canShowFilePickerTrack = false;\n canShowDropZone = false;\n\n constructor(editor) {\n const permissions = getEmbedPermissions(editor);\n const options = getFilePicker(editor, 'media');\n\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 this.canShowFilePickerTrack = permissions.filepicker && (typeof getFilePicker(editor, 'subtitle') !== 'undefined');\n this.canShowDropZone = Object.values(options.repositories).some(repository => repository.type === 'upload');\n this.editor = editor;\n this.acceptedMediaTypes = options.accepted_types;\n this.contextId = getContextId(editor);\n\n // Image options.\n const imageOptions = getFilePicker(editor, 'image');\n this.acceptedImageTypes = imageOptions.accepted_types;\n this.canShowImageFilePicker = permissions.filepicker\n && (typeof imageOptions !== 'undefined')\n && Object.keys(imageOptions.repositories).length > 0;\n }\n\n /**\n * Displays media modal accordingly.\n */\n displayDialogue = async() => {\n const [mediaType, selectedMedia] = getSelectedMediaElement(this.editor);\n this.mediaType = mediaType;\n this.selectedMedia = selectedMedia;\n\n if (this.selectedMedia) {\n // Preview the selected media.\n this.isUpdating = true;\n this.loadSelectedMedia();\n } else {\n // Create media modal.\n await this.createMediaModal();\n // Load insert media modal.\n await this.loadInsertMediaModal();\n }\n };\n\n /**\n * Load insert media modal.\n */\n loadInsertMediaModal = async() => {\n const embedHandler = new EmbedHandler(this);\n embedHandler.loadTemplatePromise(insertMediaTemplateContext(this));\n await embedHandler.registerEventListeners();\n };\n\n /**\n * Create media modal.\n */\n createMediaModal = async() => {\n this.currentModal = await EmbedModal.create({\n large: true,\n templateContext: {elementid: this.editor.getElement().id},\n });\n this.modalRoot = this.currentModal.getRoot();\n this.root = this.modalRoot[0];\n };\n\n /**\n * Load media preview based on the selected media.\n */\n loadSelectedMedia = async() => {\n let mediaSource = null;\n if (['video', 'audio'].includes(this.mediaType)) {\n mediaSource = this.selectedMedia.querySelector('source').src;\n // If the selected media has more than one sources, it has main source and alternative sources.\n const sources = this.selectedMedia.querySelectorAll('source');\n if (sources.length > 1) {\n let alternativeSources = [];\n Object.keys(sources).forEach(function(source) {\n alternativeSources.push(sources[source].src);\n });\n this.alternativeSources = alternativeSources; // Used to later check if the embedded media has alternative sources.\n }\n } else if (this.selectedMedia.classList.contains(Selectors.EMBED.externalMediaProvider)) {\n mediaSource = this.selectedMedia.href;\n this.mediaType = 'link';\n }\n\n // Load media preview.\n if (this.mediaType) {\n // Create media modal.\n await this.createMediaModal();\n // Start the spinner.\n startMediaLoading(this.root, Selectors.EMBED.type);\n\n const embedInsert = new EmbedInsert(this);\n embedInsert.init();\n embedInsert.loadMediaPreview(mediaSource);\n await (new EmbedHandler(this)).registerEventListeners();\n } else {\n // Create media modal.\n await this.createMediaModal();\n // Load insert media modal.\n this.loadInsertMediaModal();\n }\n };\n}\n"],"names":["constructor","editor","async","mediaType","selectedMedia","this","isUpdating","loadSelectedMedia","createMediaModal","loadInsertMediaModal","embedHandler","EmbedHandler","loadTemplatePromise","registerEventListeners","currentModal","EmbedModal","create","large","templateContext","elementid","getElement","id","modalRoot","getRoot","root","mediaSource","includes","querySelector","src","sources","querySelectorAll","length","alternativeSources","Object","keys","forEach","source","push","classList","contains","Selectors","EMBED","externalMediaProvider","href","type","embedInsert","EmbedInsert","init","loadMediaPreview","permissions","options","canShowFilePicker","filepicker","repositories","canShowFilePickerTrack","canShowDropZone","values","some","repository","acceptedMediaTypes","accepted_types","contextId","imageOptions","acceptedImageTypes","canShowImageFilePicker"],"mappings":"uuBAsCIA,YAAYC,sCALH,gDACW,kDACK,2CACP,2CA2BAC,gBACPC,UAAWC,gBAAiB,yCAAwBC,KAAKJ,aAC3DE,UAAYA,eACZC,cAAgBA,cAEjBC,KAAKD,oBAEAE,YAAa,OACbC,4BAGCF,KAAKG,yBAELH,KAAKI,wEAOIP,gBACbQ,aAAe,IAAIC,2BAAaN,MACtCK,aAAaE,qBAAoB,4CAA2BP,aACtDK,aAAaG,qEAMJX,eACVY,mBAAqBC,oBAAWC,OAAO,CACxCC,OAAO,EACPC,gBAAiB,CAACC,UAAWd,KAAKJ,OAAOmB,aAAaC,WAErDC,UAAYjB,KAAKS,aAAaS,eAC9BC,KAAOnB,KAAKiB,UAAU,gDAMXpB,cACZuB,YAAc,QACd,CAAC,QAAS,SAASC,SAASrB,KAAKF,WAAY,CAC7CsB,YAAcpB,KAAKD,cAAcuB,cAAc,UAAUC,UAEnDC,QAAUxB,KAAKD,cAAc0B,iBAAiB,aAChDD,QAAQE,OAAS,EAAG,KAChBC,mBAAqB,GACzBC,OAAOC,KAAKL,SAASM,SAAQ,SAASC,QAClCJ,mBAAmBK,KAAKR,QAAQO,QAAQR,aAEvCI,mBAAqBA,yBAEvB3B,KAAKD,cAAckC,UAAUC,SAASC,mBAAUC,MAAMC,yBAC7DjB,YAAcpB,KAAKD,cAAcuC,UAC5BxC,UAAY,WAIjBE,KAAKF,UAAW,OAEVE,KAAKG,kDAEOH,KAAKmB,KAAMgB,mBAAUC,MAAMG,YAEvCC,YAAc,IAAIC,yBAAYzC,MACpCwC,YAAYE,OACZF,YAAYG,iBAAiBvB,mBACtB,IAAId,2BAAaN,MAAOQ,oCAGzBR,KAAKG,wBAENC,gCAlGHwC,aAAc,gCAAoBhD,QAClCiD,SAAU,2BAAcjD,OAAQ,cAGjCkD,kBAAoBF,YAAYG,iBACV,IAAZF,SACRjB,OAAOC,KAAKgB,QAAQG,cAActB,OAAS,OAC7CuB,uBAAyBL,YAAYG,iBAA4D,KAAtC,2BAAcnD,OAAQ,iBACjFsD,gBAAkBtB,OAAOuB,OAAON,QAAQG,cAAcI,MAAKC,YAAkC,WAApBA,WAAWd,YACpF3C,OAASA,YACT0D,mBAAqBT,QAAQU,oBAC7BC,WAAY,0BAAa5D,cAGxB6D,cAAe,2BAAc7D,OAAQ,cACtC8D,mBAAqBD,aAAaF,oBAClCI,uBAAyBf,YAAYG,iBACV,IAAjBU,cACR7B,OAAOC,KAAK4B,aAAaT,cAActB,OAAS"}PK/<\Ƶ&'&'amd/build/mediabase.min.js.mapnu[{"version":3,"file":"mediabase.min.js","sources":["../src/mediabase.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 class helpers for image and embed.\n *\n * @module tiny_media/mediabase\n * @copyright 2024 Stevani Andolo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {\n isPercentageValue,\n hideElements,\n showElements,\n} from './helpers';\nimport Selectors from './selectors';\n\nexport class MediaBase {\n\n /**\n * Handles the selection of media size options and updates the form inputs accordingly.\n *\n * @param {string} option - The selected media size option (\"original\" or \"custom\").\n */\n sizeChecked = async(option) => {\n const widthInput = this.root.querySelector(Selectors[this.selectorType].elements.width);\n const heightInput = this.root.querySelector(Selectors[this.selectorType].elements.height);\n if (option === \"original\") {\n this.sizeOriginalChecked();\n widthInput.value = this.mediaDimensions.width;\n heightInput.value = this.mediaDimensions.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 and selectorType = IMAGE,\n // then check the Keep proportion checkbox.\n if (\n this.selectorType === Selectors.IMAGE.type &&\n this.currentWidth === this.mediaDimensions.width &&\n this.currentHeight === this.mediaDimensions.height\n ) {\n const constrainField = this.root.querySelector(Selectors[this.selectorType].elements.constrain);\n constrainField.checked = true;\n }\n }\n this.autoAdjustSize();\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[this.selectorType].elements.sizeOriginal).checked = true;\n this.root.querySelector(Selectors[this.selectorType].elements.sizeCustom).checked = false;\n hideElements(Selectors[this.selectorType].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[this.selectorType].elements.sizeOriginal).checked = false;\n this.root.querySelector(Selectors[this.selectorType].elements.sizeCustom).checked = true;\n showElements(Selectors[this.selectorType].elements.properties, this.root);\n }\n\n /**\n * Auto adjust the media width/height.\n * It is put here so image.js and/or friends can extend this class and call this for media proportion.\n *\n * @param {boolean} forceHeight Whether set by height or not\n */\n autoAdjustSize = (forceHeight = false) => {\n // If we do not know the media size, do not do anything.\n if (!this.mediaDimensions) {\n return;\n }\n\n const widthField = this.root.querySelector(Selectors[this.selectorType].elements.width);\n const heightField = this.root.querySelector(Selectors[this.selectorType].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.mediaDimensions[fieldData.type] / 100 * fieldData.percentValue;\n } else {\n fieldData.pixelSize = parseInt(fieldData.field.value, 10);\n fieldData.percentValue = fieldData.pixelSize / this.mediaDimensions[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.mediaDimensions[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[this.selectorType].elements.constrain); // Only image.\n if ((constrainField && constrainField.checked) || this.mediaType === 'video') {\n const keyField = getKeyField();\n const relativeField = getRelativeField();\n // We are keeping the media 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.mediaDimensions[keyField.type] * this.mediaDimensions[relativeField.type]\n );\n relativeField.field.value = relativeField.pixelSize;\n }\n }\n\n if (this.selectorType === Selectors.IMAGE.type) {\n // Store the custom width and height to reuse.\n this.currentWidth = Number(widthField.value) !== this.mediaDimensions.width ? widthField.value : this.currentWidth;\n this.currentHeight = Number(heightField.value) !== this.mediaDimensions.height ? heightField.value : this.currentHeight;\n }\n };\n}\n"],"names":["async","widthInput","this","root","querySelector","Selectors","selectorType","elements","width","heightInput","height","option","sizeOriginalChecked","value","mediaDimensions","sizeCustomChecked","currentWidth","currentHeight","IMAGE","type","constrain","checked","autoAdjustSize","forceHeight","_this","widthField","heightField","normalizeFieldData","fieldData","isPercentageValue","field","percentValue","parseInt","pixelSize","getKeyField","currentValue","getRelativeField","constrainField","mediaType","keyField","relativeField","Math","round","Number","sizeOriginal","sizeCustom","properties"],"mappings":"kgBAoCkBA,MAAAA,eACJC,WAAaC,KAAKC,KAAKC,cAAcC,mBAAUH,KAAKI,cAAcC,SAASC,OAC3EC,YAAcP,KAAKC,KAAKC,cAAcC,mBAAUH,KAAKI,cAAcC,SAASG,WACnE,aAAXC,YACKC,sBACLX,WAAWY,MAAQX,KAAKY,gBAAgBN,MACxCC,YAAYI,MAAQX,KAAKY,gBAAgBJ,YACtC,GAAe,WAAXC,cACFI,oBACLd,WAAWY,MAAQX,KAAKc,aACxBP,YAAYI,MAAQX,KAAKe,cAKrBf,KAAKI,eAAiBD,mBAAUa,MAAMC,MACtCjB,KAAKc,eAAiBd,KAAKY,gBAAgBN,OAC3CN,KAAKe,gBAAkBf,KAAKY,gBAAgBJ,QAC9C,CACyBR,KAAKC,KAAKC,cAAcC,mBAAUH,KAAKI,cAAcC,SAASa,WACtEC,SAAU,OAG5BC,2DA2BQ,eAACC,wEAETC,MAAKV,6BAIJW,WAAaD,MAAKrB,KAAKC,cAAcC,mBAAUmB,MAAKlB,cAAcC,SAASC,OAC3EkB,YAAcF,MAAKrB,KAAKC,cAAcC,mBAAUmB,MAAKlB,cAAcC,SAASG,QAE5EiB,mBAAsBC,YACxBA,UAAUC,mBAAoB,8BAAkBD,UAAUE,MAAMjB,OAC5De,UAAUC,mBACVD,UAAUG,aAAeC,SAASJ,UAAUE,MAAMjB,MAAO,IACzDe,UAAUK,UAAYT,MAAKV,gBAAgBc,UAAUT,MAAQ,IAAMS,UAAUG,eAE7EH,UAAUK,UAAYD,SAASJ,UAAUE,MAAMjB,MAAO,IACtDe,UAAUG,aAAeH,UAAUK,UAAYT,MAAKV,gBAAgBc,UAAUT,MAAQ,KAGnFS,WAGLM,YAAc,WAeVC,aAbEZ,YACO,CACHO,MAAOJ,YACPP,KAAM,UAGH,CACHW,MAAOL,WACPN,KAAM,eAMe,KAA7BgB,aAAaL,MAAMjB,QACnBsB,aAAaL,MAAMjB,MAAQW,MAAKV,gBAAgBqB,aAAahB,OAG1DQ,mBAAmBQ,eAGxBC,iBAAmB,IAEVT,mBADPJ,YAC0B,CACtBO,MAAOL,WACPN,KAAM,SAGgB,CACtBW,MAAOJ,YACPP,KAAM,WAMZkB,eAAiBb,MAAKrB,KAAKC,cAAcC,mBAAUmB,MAAKlB,cAAcC,SAASa,cAChFiB,gBAAkBA,eAAehB,SAA+B,UAAnBG,MAAKc,UAAuB,OACpEC,SAAWL,cACXM,cAAgBJ,mBAGlBG,SAASV,mBAETW,cAAcV,MAAMjB,MAAQ0B,SAAST,MAAMjB,MAC3C2B,cAAcT,aAAeQ,SAASR,eAEtCS,cAAcP,UAAYQ,KAAKC,MAC3BH,SAASN,UAAYT,MAAKV,gBAAgByB,SAASpB,MAAQK,MAAKV,gBAAgB0B,cAAcrB,OAElGqB,cAAcV,MAAMjB,MAAQ2B,cAAcP,WAI9CT,MAAKlB,eAAiBD,mBAAUa,MAAMC,OAEtCK,MAAKR,aAAe2B,OAAOlB,WAAWZ,SAAWW,MAAKV,gBAAgBN,MAAQiB,WAAWZ,MAAQW,MAAKR,aACtGQ,MAAKP,cAAgB0B,OAAOjB,YAAYb,SAAWW,MAAKV,gBAAgBJ,OAASgB,YAAYb,MAAQW,MAAKP,kBAtGlHL,2BACST,KAAKC,cAAcC,mBAAUH,KAAKI,cAAcC,SAASqC,cAAcvB,SAAU,OACjFlB,KAAKC,cAAcC,mBAAUH,KAAKI,cAAcC,SAASsC,YAAYxB,SAAU,4BACvEhB,mBAAUH,KAAKI,cAAcC,SAASuC,WAAY5C,KAAKC,MAMxEY,yBACSZ,KAAKC,cAAcC,mBAAUH,KAAKI,cAAcC,SAASqC,cAAcvB,SAAU,OACjFlB,KAAKC,cAAcC,mBAAUH,KAAKI,cAAcC,SAASsC,YAAYxB,SAAU,4BACvEhB,mBAAUH,KAAKI,cAAcC,SAASuC,WAAY5C,KAAKC"}PK/<\IR amd/build/usedfiles.min.jsnu[define("tiny_media/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_media/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(userContext,itemId,elementId){this.files=this.getFiles(),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_media/missingfiles",{missingFiles:missingFiles}).then((_ref3=>{let{html:html,js:js}=_ref3;Templates.replaceNodeContents(form.querySelector(".missing-files"),html,js)}))}getFiles(){const files={};return document.querySelectorAll('input[type=checkbox][name^="deletefile"]').forEach((input=>{files[input.dataset.filename]=input.dataset.filehash})),files}}_exports.init=(files,usercontext,itemid,elementid)=>{const manager=new UsedFileManager(files,usercontext,itemid,elementid);return manager.updateFiles(),manager}})); //# sourceMappingURL=usedfiles.min.js.mapPK/<\ amd/build/plugin.min.js.mapnu[{"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_media/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';\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 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","resolve","Configuration"],"mappings":";;;;;;;2OA+Be,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,QAEPR,kBAIXW,QAAQ,WAAIP,6BAAoBQ"}PK/<\3A.amd/build/embedmodal.min.js.mapnu[{"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_media/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/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.removeOnClose = true;\n modalConfig.show = true;\n\n super.configure(modalConfig);\n }\n}\n"],"names":["EmbedModal","Modal","registerEventListeners","registerCloseOnSave","registerCloseOnCancel","configure","modalConfig","removeOnClose","show","component"],"mappings":"8ZA0BqBA,mBAAmBC,eAIpCC,+BAEUA,8BAGDC,2BACAC,wBAGTC,UAAUC,aACNA,YAAYC,eAAgB,EAC5BD,YAAYE,MAAO,QAEbH,UAAUC,iEAjBHN,4BACAS,6CADAT,gCAEIS"}PK/<\DXamd/build/helpers.min.jsnu[define("tiny_media/helpers",["exports","core/templates","./selectors","core/config"],(function(_exports,_templates,_selectors,_config){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Tiny media plugin helpers for image and embed. * * @module tiny_media/helpers * @copyright 2024 Stevani Andolo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.stopMediaLoading=_exports.startMediaLoading=_exports.sourceTypeChecked=_exports.showElements=_exports.setPropertiesFromData=_exports.setFilenameLabel=_exports.isValidUrl=_exports.isPercentageValue=_exports.isExternalUrl=_exports.hideElements=_exports.getFileName=_exports.footer=_exports.body=void 0,_templates=_interopRequireDefault(_templates),_selectors=_interopRequireDefault(_selectors),_config=_interopRequireDefault(_config);_exports.body=async(templateContext,root)=>_templates.default.renderForPromise(templateContext.bodyTemplate,{...templateContext}).then((_ref=>{let{html:html,js:js}=_ref;_templates.default.replaceNodeContents(root.querySelector(_selectors.default[templateContext.selector].elements.bodyTemplate),html,js)})).catch((error=>{window.console.log(error)}));_exports.footer=async(templateContext,root)=>_templates.default.renderForPromise(templateContext.footerTemplate,{...templateContext}).then((_ref2=>{let{html:html,js:js}=_ref2;_templates.default.replaceNodeContents(root.querySelector(_selectors.default[templateContext.selector].elements.footerTemplate),html,js)})).catch((error=>{window.console.log(error)}));_exports.setPropertiesFromData=async(instance,data)=>{for(const property in data)"function"!=typeof data[property]&&(instance[property]=data[property]);return instance};_exports.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);const 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.hideElements=hideElements;const 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.showElements=showElements;_exports.startMediaLoading=(root,selector)=>{showElements(_selectors.default[selector].elements.loaderIcon,root);const elementsToHide=[_selectors.default[selector].elements.insertMedia,_selectors.default[selector].elements.urlWarning,_selectors.default[selector].elements.modalFooter];hideElements(elementsToHide,root)};_exports.stopMediaLoading=(root,selector)=>{hideElements(_selectors.default[selector].elements.loaderIcon,root);const elementsToShow=[_selectors.default[selector].elements.insertMedia,_selectors.default[selector].elements.modalFooter];showElements(elementsToShow,root)};const isExternalUrl=url=>!1===new RegExp("".concat(_config.default.wwwroot)).test(url);_exports.isExternalUrl=isExternalUrl;const setFilenameLabel=props=>{const urlLabelEle=props.root.querySelector(props.fileNameSelector);urlLabelEle&&(urlLabelEle.innerHTML=props.label,urlLabelEle.setAttribute("title",props.label))};_exports.setFilenameLabel=setFilenameLabel;_exports.sourceTypeChecked=props=>{if(isExternalUrl(props.source))props.label=decodeURI(props.source);else{const filename=props.source.split("/").pop().split("?")[0];props.label=decodeURI(filename)}setFilenameLabel(props)};_exports.getFileName=fileLabel=>{if(fileLabel.includes("/")){const split=fileLabel.split("/");let fileName=split[split.length-1];return fileName=fileName.split("."),fileName.length>1?decodeURI(fileName.slice(0,fileName.length-1).join(".")):decodeURI(fileName[0])}return decodeURI(fileLabel.split(".")[0])};_exports.isPercentageValue=value=>value.match(/\d+%/)})); //# sourceMappingURL=helpers.min.js.mapPK/<\H§amd/build/embedmodal.min.jsnu[define("tiny_media/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.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/embed_media_modal")),_exports.default})); //# sourceMappingURL=embedmodal.min.js.mapPK/<\ amd/build/manager.min.jsnu[define("tiny_media/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_media"),body:_templates.default.render("tiny_media/mm_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("mm-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/media/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.mapPK/<\Jamd/build/manager.min.js.mapnu[{"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_media/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_media'),\n body: Templates.render('tiny_media/mm_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('mm-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/media/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":"gmDAmCIA,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,cAC3CC,KAAMC,mBAAUC,OAAO,uBAAwB,CAC3CC,IAAKC,KAAKC,iBAEdC,eAAe,EACfC,MAAM,WAEVZ,MAAMa,UAAUC,GAAGC,YAAYC,cAAc,UACpCC,wBAGTC,SAASC,cAAc,aAAaC,MAAMC,2BACnCrB,MAIXiB,2BACUK,OAASJ,SAASK,eAAe,aACvCD,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,sDAC9BH,IAAII,aAAaC,OAAO,YAAa5B,KAAKhB,OAAO6C,aAAaC,QACzD,MAAMC,OAAO/B,KAAKd,KACnBqC,IAAII,aAAaC,OAAOG,IAAK/B,KAAKd,KAAK6C,aAEpCR,IAAIS"}PK/<\8 t  tests/plugininfo_test.phpnu[. declare(strict_types=1); namespace tiny_media; use advanced_testcase; /** * Unit tests for the \tiny_media\plugininfo class. * * @package tiny_media * @covers \tiny_media\plugininfo::is_enabled_for_external * @copyright 2025 Moodle Pty Ltd * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class plugininfo_test extends advanced_testcase { /** * Basic setup for tests. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(true); } /** * Test the is_enabled_for_external method. * * @dataProvider is_enabled_for_external_provider * @param bool $guest True to use guest user. * @param bool $expectedenabled Expected result. * @return void */ public function test_is_enabled_for_external(bool $guest, bool $expectedenabled): void { global $CFG; $generator = $this->getDataGenerator(); if ($guest) { $this->setGuestUser(); } else { $user = $generator->create_user(); $this->setUser($user); } $context = \context_system::instance(); $this->assertEquals($expectedenabled, plugininfo::is_enabled_for_external($context, ['pluginname' => 'media'])); } /** * Data provider for test_is_enabled_for_external. * * @return array */ public static function is_enabled_for_external_provider(): array { return [ ['guest' => false, 'expectedenabled' => true], ['guest' => true, 'expectedenabled' => false], ]; } } PK/<\-e++tests/behat/media.featurenu[@editor @editor_tiny @tiny_media @javascript Feature: Use the TinyMCE editor to upload a media file In order to work with media files As a user I need to be able to upload and change settings of media files Background: 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 Scenario: Clicking on the media button in the TinyMCE editor opens the insert media dialog Then "Insert media" "dialogue" should exist And "Browse repositories" "button" should exist in the "Insert media" "dialogue" And I click on "Browse repositories" "button" in the "Insert media" "dialogue" And "File picker" "dialogue" should exist @_file_upload Scenario Outline: Insert and update media in the TinyMCE editor Given I click on "Browse repositories" "button" When I upload "/lib/editor/tiny/tests/behat/fixtures/" to the file picker for TinyMCE Then "Media details" "dialogue" should exist And "Media title" "field" should exist in the "Media details" "dialogue" And "Add custom thumbnail" "button" in the "Media details" "dialogue" And "Subtitles and captions" "link" should exist in the "Media details" "dialogue" And the field "Media title" in the "Media details" "dialogue" matches value "" And the field "Show controls" in the "Media details" "dialogue" matches value "1" And the field "Autoplay" in the "Media details" "dialogue" matches value "0" And the field "Muted" in the "Media details" "dialogue" matches value "0" And the field "Loop" in the "Media details" "dialogue" matches value "0" And And And I click on "Delete media" "button" in the "Media details" "dialogue" And "Delete media" "dialogue" should exist And I click on "Delete" "button" in the "Delete media" "dialogue" And "Insert media" "dialogue" should exist And I click on "Browse repositories" "button" And I upload "/lib/editor/tiny/tests/behat/fixtures/" to the file picker for TinyMCE And I click on "Overwrite" "button" And I set the field "Media title" to "" And I set the field "Autoplay" to "1" And I set the field "Muted" to "1" And I set the field "Loop" to "1" And And And I click on "Subtitles and captions" "link" in the "Media details" "dialogue" And I click on "Browse repositories..." "button" in the "Media details" "dialogue" And I upload "/lib/editor/tiny/tests/behat/fixtures/" to the file picker for TinyMCE And I set the field "Language" in the "Media details" "dialogue" to "" And I set the field "Label" in the "Media details" "dialogue" to "" And I click on "Save" "button" in the "Media details" "dialogue" And I switch to the "Description" TinyMCE editor iframe And "//*[contains(@data-id, 'id_description_editor')]//[@title='' and @autoplay='autoplay' and @loop='loop' and @muted='true' and @controls='controls']" "xpath_element" should exist And "//*[contains(@data-id, 'id_description_editor')]////source[contains(@src, '')]" "xpath_element" should exist And "//*[contains(@data-id, 'id_description_editor')]////track[contains(@src, '') and @kind='subtitles' and contains(@srclang, '') and @label='']" "xpath_element" should exist And I switch to the main frame 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 the field "Media title" in the "Media details" "dialogue" matches value "" And the field "Show controls" in the "Media details" "dialogue" matches value "1" And the field "Autoplay" in the "Media details" "dialogue" matches value "1" And the field "Muted" in the "Media details" "dialogue" matches value "1" And the field "Loop" in the "Media details" "dialogue" matches value "1" And And And I click on "Subtitles and captions" "link" in the "Media details" "dialogue" And the field "Subtitle track URL" in the "Media details" "dialogue" does not match value "" And the field "Language" in the "Media details" "dialogue" matches value "" And the field "Label" in the "Media details" "dialogue" matches value "" Examples: | mediatype | fixturefile | fixturetitle | newfixturetitle | subtitlefile | subtitletitle | subtitlelangcode | subtitlelang | thumbnailaction | originalsizeverify | customsizeverify1 | customsizeverify2 | customsizeverify3 | customsizestep | customsizeaction | | video | moodle-logo.mp4 | moodle-logo | Moodle LMS Logo | subtitle-sample.vtt | Subtitle sample for video | he | Hebrew | should exist | the field "Original size" in the "Media details" "dialogue" matches value "1" | the field "Custom size" in the "Media details" "dialogue" matches value "0" | the field "Custom size" in the "Media details" "dialogue" matches value "1" | the field "Width" in the "Media details" "dialogue" matches value "300" | I click on "Custom size" "radio" in the "Media details" "dialogue" | I set the field "Width" in the "Media details" "dialogue" to "300" | | audio | audio-sample.mp3 | audio-sample | Sample Audio File | subtitle-sample.vtt | Subtitle sample for audio | fr | French | should not exist | "Original size" "field" should not exist in the "Media details" "dialogue" | "Custom size" "field" should not exist in the "Media details" "dialogue" | "Custom size" "field" should not exist in the "Media details" "dialogue" | "Width" "field" should not exist in the "Media details" "dialogue" | "Original size" "field" should not exist in the "Media details" "dialogue" | "Custom size" "field" should not exist in the "Media details" "dialogue" | @_file_upload Scenario: Add custom thumbnail to a video in TinyMCE editor Given I click on "Browse repositories" "button" in the "Insert media" "dialogue" And I upload "/lib/editor/tiny/tests/behat/fixtures/moodle-logo.mp4" to the file picker for TinyMCE When I click on "Add custom thumbnail" "button" in the "Media details" "dialogue" Then "Insert media thumbnail" "dialogue" should exist And I click on "Browse repositories" "button" in the "Insert media thumbnail" "dialogue" And I upload "lib/editor/tiny/tests/behat/fixtures/moodle-logo.png" to the file picker for TinyMCE And "Media thumbnail" "dialogue" should exist And "tiny-media-thumbnail-preview" "region" should exist in the "Media thumbnail" "dialogue" And I click on "Delete media thumbnail" "button" in the "Media thumbnail" "dialogue" And "Delete media thumbnail" "dialogue" should exist And I click on "Delete" "button" in the "Delete media thumbnail" "dialogue" And I click on "Browse repositories" "button" in the "Insert media thumbnail" "dialogue" And I upload "lib/editor/tiny/tests/behat/fixtures/moodle-logo.png" to the file picker for TinyMCE And I click on "Overwrite" "button" in the "File exists" "dialogue" And I click on "Next" "button" in the "Media thumbnail" "dialogue" But "Add custom thumbnail" "button" should not exist in the "Media details" "dialogue" And "Change thumbnail" "button" should exist in the "Media details" "dialogue" And I click on "Delete thumbnail" "button" in the "Media details" "dialogue" And I click on "Delete" "button" in the "Delete thumbnail" "dialogue" And "Media details" "dialogue" should exist And "Change thumbnail" "button" should not exist in the "Media details" "dialogue" And "Delete thumbnail" "button" should not exist in the "Media details" "dialogue" And I click on "Add custom thumbnail" "button" in the "Media details" "dialogue" And "Insert media thumbnail" "dialogue" should exist And I click on "Browse repositories" "button" in the "Insert media thumbnail" "dialogue" And I upload "lib/editor/tiny/tests/behat/fixtures/moodle-logo.png" to the file picker for TinyMCE And I click on "Overwrite" "button" in the "File exists" "dialogue" And I click on "Next" "button" in the "Media thumbnail" "dialogue" And I click on "Save" "button" in the "Media details" "dialogue" And I switch to the "Description" TinyMCE editor iframe And "//*[contains(@data-id, 'id_description_editor')]//video[contains(@poster, 'moodle-logo.png')]" "xpath_element" should exist Scenario: Embed external video link - External video service Given the "mediaplugin" filter is "on" And I enable "youtube" "media" plugin And I disable "videojs" "media" plugin And 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 When I set the field "Or add via URL" to "https://www.youtube.com/watch?v=JeimE8Wz6e4" And I click on "Add" "button" in the "Insert media" "dialogue" Then "Media details" "dialogue" should exist And "Media title" "field" should exist in the "Media details" "dialogue" And "Show controls" "field" should not exist in the "Media details" "dialogue" And "Autoplay" "field" should not exist in the "Media details" "dialogue" And "Muted" "field" should not exist in the "Media details" "dialogue" And "Loop" "field" should not exist in the "Media details" "dialogue" And the field "Media title" in the "Media details" "dialogue" matches value "https://www.youtube.com/watch?v=JeimE8Wz6e4" And I set the field "Media title" to "Hey, that is pretty good!" And I click on "Save" "button" in the "Media details" "dialogue" And I switch to the "Description" TinyMCE editor iframe And "//*[contains(@data-id, 'id_description_editor')]//a[@class='external-media-provider' and @href='https://www.youtube.com/watch?v=JeimE8Wz6e4' and normalize-space(text())='Hey, that is pretty good!']" "xpath_element" should exist And I switch to the main frame And I select the "a" element in position "0" of the "Description" TinyMCE editor And I click on the "Multimedia" button for the "Description" TinyMCE editor And the field "Media title" in the "Media details" "dialogue" matches value "Hey, that is pretty good!" And I click on "Cancel" "button" in the "Media details" "dialogue" And I press "Update profile" And "//span[contains(@class, 'mediaplugin_youtube')]//iframe[@title='Hey, that is pretty good!']" "xpath_element" should exist PK/<\)22tests/behat/image.featurenu[@editor @editor_tiny @tiny_media @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" @_file_upload Scenario: Set the alt text to the maximum and below the maximum length 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/tinyscreenshot.png" to the file picker for TinyMCE When I set the field "How would you describe this image to someone who can't see it?" to "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet." Then I should see "750" in the "#currentcount" "css_element" And I set the field "How would you describe this image to someone who can't see it?" to "Lorem ipsum dolor sit amet." And I should see "27" in the "#currentcount" "css_element" @javascript Scenario: Permissions can be configured to control access to media Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | teacher2 | Teacher | 2 | teacher2@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "roles" exist: | name | shortname | description | archetype | | Custom teacher | custom1 | Limited permissions | editingteacher | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher2 | C1 | custom1 | And the following "activity" exists: | activity | assign | | course | C1 | | name | Test assignment | And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | tiny/media:use | Prohibit | custom1 | Course | C1 | # Check plugin access as a role with prohibited permissions. And I log in as "teacher2" And I am on the "Test assignment" Activity page And I navigate to "Settings" in current page administration When I click on the "Insert" menu item for the "Activity instructions" TinyMCE editor Then I should not see "Multimedia" # Check plugin access as a role with allowed permissions. And I log in as "teacher1" And I am on the "Test assignment" Activity page And I navigate to "Settings" in current page administration And I click on the "Insert" menu item for the "Activity instructions" TinyMCE editor And I should see "Multimedia" PKϲ\{xxupload_photos.phpnu[PKϲ\ PBllfile_categories.phpnu[PKϲ\as  hfiles.phpnu[PKϲ\$ videos.phpnu[PKϲ\˼1,, |"photos.phpnu[PKϲ\ۓ &albums.phpnu[PK/<\Z]P 4styles.cssnu[PK/<\U>db/services.phpnu[PK/<\z =ss Cdb/access.phpnu[PK/<\w< aHlang/en/deprecated.txtnu[PK/<\\b -Jlang/en/tiny_media.phpnu[PK/<\K4>% % 'kpix/filemanager.svgnu[PK/<\zҳ upix/icon.svgnu[PK/<\  wversion.phpnu[PK/<\ |manage.phpnu[PK/<\d"classes/form/manage_files_form.phpnu[PK/<\!  classes/external/preview.phpnu[PK/<\FXX^classes/privacy/provider.phpnu[PK/<\Qclasses/plugininfo.phpnu[PK/<\ڌ)Otemplates/missingfiles.mustachenu[PK/<\>άyggbtemplates/mm_iframe.mustachenu[PK/<\b=ootemplates/image.mustachenu[PK/<\O  3templates/insert_image_modal_insert_footer.mustachenu[PK/<\_kk-1templates/insert_image_modal_details.mustachenu[PK/<\t,templates/insert_image_modal_insert.mustachenu[PK/<\;FԱ``*templates/embed/embed_media_audio.mustachenu[PK/<\V46templates/embed/insert_media_subtitle_caption.mustachenu[PK/<\vKQ)templates/embed/embed_media_link.mustachenu[PK/<\UM/templates/embed/body/insert_media_body.mustachenu[PK/<\pZ((0ttemplates/embed/body/media_details_body.mustachenu[PK/<\ 2Gtemplates/embed/body/media_thumbnail_body.mustachenu[PK/<\")yy*Mtemplates/embed/embed_media_modal.mustachenu[PK/<\I.JJ*Vtemplates/embed/embed_media_video.mustachenu[PK/<\yܧ~.Z]templates/embed/metadata/descriptions.mustachenu[PK/<\__*kxtemplates/embed/metadata/metadata.mustachenu[PK/<\E2zz+$templates/embed/metadata/subtitles.mustachenu[PK/<\#SS*templates/embed/metadata/chapters.mustachenu[PK/<\\QQ*templates/embed/metadata/captions.mustachenu[PK/<\ "} } 3Qtemplates/embed/footer/insert_media_footer.mustachenu[PK/<\4CC61templates/embed/footer/media_thumbnail_footer.mustachenu[PK/<\62@4templates/embed/footer/media_details_footer.mustachenu[PK/<\]  4templates/insert_image_modal_details_footer.mustachenu[PK/<\-ذ%Btemplates/insert_image_modal.mustachenu[PK/<\hL G amd/src/configuration.jsnu[PK/<\{USMMsamd/src/imagedetails.jsnu[PK/<\eDbcamd/src/mediabase.jsnu[PK/<\3+QQamd/src/embed.jsnu[PK/<\.4LL-amd/src/imagehelpers.jsnu[PK/<\# # amd/src/options.jsnu[PK/<\~%amd/src/embedmodal.jsnu[PK/<\5QQXamd/src/common.jsnu[PK/<\j ͐88amd/src/commands.jsnu[PK/<\[:''eamd/src/imageinsert.jsnu[PK/<\KKamd/src/plugin.jsnu[PK/<\b$$ amd/src/image.jsnu[PK/<\ 0y>y>o+amd/src/embed/embedpreview.jsnu[PK/<\A""5jamd/src/embed/embedinsert.jsnu[PK/<\PWW%amd/src/embed/embedthumbnailinsert.jsnu[PK/<\WC&-amd/src/embed/embedthumbnailpreview.jsnu[PK/<\R†%%Namd/src/embed/embedhelpers.jsnu[PK/<\$/STST!amd/src/embed/embedhandler.jsnu[PK/<\jc ;amd/src/manager.jsnu[PK/<\RWGamd/src/selectors.jsnu[PK/<\2aamd/src/imagemodal.jsnu[PK/<\J ))sgamd/src/usedfiles.jsnu[PK/<\絥Zvamd/src/helpers.jsnu[PK/<\V~~amd/build/common.min.js.mapnu[PK/<\Üamd/build/usedfiles.min.js.mapnu[PK/<\+<̴amd/build/imagemodal.min.jsnu[PK/<\"Oך##amd/build/imagemodal.min.js.mapnu[PK/<\^amd/build/imageinsert.min.jsnu[PK/<\#rr!amd/build/imagedetails.min.js.mapnu[PK/<\[A_77Lamd/build/configuration.min.jsnu[PK/<\*55Ramd/build/image.min.js.mapnu[PK/<\X ȳ??|amd/build/imagehelpers.min.jsnu[PK/<\kamd/build/options.min.jsnu[PK/<\JPcamd/build/image.min.jsnu[PK/<\mI!!pamd/build/commands.min.js.mapnu[PK/<\!Camd/build/common.min.jsnu[PK/<\b1M+M+amd/build/helpers.min.js.mapnu[PK/<\x=i:i: amd/build/imageinsert.min.js.mapnu[PK/<\6y$$<amd/build/selectors.min.js.mapnu[PK/<\m3BB/aamd/build/plugin.min.jsnu[PK/<\ iamd/build/embed.min.jsnu[PK/<\DH<!wamd/build/imagehelpers.min.js.mapnu[PK/<\#İVV"amd/build/configuration.min.js.mapnu[PK/<\si--/amd/build/embed/embedthumbnailinsert.min.js.mapnu[PK/<\۱l..&amd/build/embed/embedinsert.min.js.mapnu[PK/<\oQ9Q9'amd/build/embed/embedhelpers.min.js.mapnu[PK/<\W[ ZZ'9amd/build/embed/embedpreview.min.js.mapnu[PK/<\"%j%j%#amd/build/embed/embedpreview.min.jsnu[PK/<\12 2 ,̹amd/build/embed/embedthumbnailpreview.min.jsnu[PK/<\f66#Zamd/build/embed/embedhandler.min.jsnu[PK/<\%W2{{+amd/build/embed/embedthumbnailinsert.min.jsnu[PK/<\ܼF"amd/build/embed/embedinsert.min.jsnu[PK/<\v9#w"amd/build/embed/embedhelpers.min.jsnu[PK/<\6T||';amd/build/embed/embedhandler.min.js.mapnu[PK/<\X&0amd/build/embed/embedthumbnailpreview.min.js.mapnu[PK/<\ s aamd/build/options.min.js.mapnu[PK/<\00bamd/build/mediabase.min.jsnu[PK/<\fXkkamd/build/commands.min.jsnu[PK/<\3ZUamd/build/selectors.min.jsnu[PK/<\з,, amd/build/imagedetails.min.jsnu[PK/<\U2< amd/build/embed.min.js.mapnu[PK/<\Ƶ&'&'[ amd/build/mediabase.min.js.mapnu[PK/<\IR d amd/build/usedfiles.min.jsnu[PK/<\  amd/build/plugin.min.js.mapnu[PK/<\3A.M amd/build/embedmodal.min.js.mapnu[PK/<\DXP amd/build/helpers.min.jsnu[PK/<\H§< amd/build/embedmodal.min.jsnu[PK/<\ ( amd/build/manager.min.jsnu[PK/<\J3 amd/build/manager.min.js.mapnu[PK/<\8 t   tests/plugininfo_test.phpnu[PK/<\-e++W tests/behat/media.featurenu[PK/<\)22x tests/behat/image.featurenu[PKss)(