mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-02-08 08:22:59 -05:00
all!
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
|
||||
import { getTinyMCE } from 'editor_tiny/loader';
|
||||
import { component } from './common';
|
||||
import IframeEmbed from './iframeembed';
|
||||
import { getString } from 'core/str';
|
||||
|
||||
export const getSetup = async () => {
|
||||
const [tinyMCE, buttonTitle, menuTitle] = await Promise.all([
|
||||
getTinyMCE(),
|
||||
getString('insertmedia', component),
|
||||
getString('mediacms', component),
|
||||
]);
|
||||
|
||||
return (editor) => {
|
||||
const iframeEmbed = new IframeEmbed(editor);
|
||||
|
||||
editor.ui.registry.addButton(component, {
|
||||
icon: 'embed',
|
||||
tooltip: buttonTitle,
|
||||
onAction: () => iframeEmbed.displayDialogue(),
|
||||
});
|
||||
|
||||
editor.ui.registry.addMenuItem(component, {
|
||||
icon: 'embed',
|
||||
text: menuTitle,
|
||||
onAction: () => iframeEmbed.displayDialogue(),
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
export const component = 'tiny_mediacms';
|
||||
export const pluginName = 'mediacms';
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
export const configure = (instanceConfig) => {
|
||||
return {
|
||||
mediacmsurl: instanceConfig.mediacmsurl,
|
||||
launchUrl: instanceConfig.launchUrl,
|
||||
lti: instanceConfig.lti
|
||||
};
|
||||
};
|
||||
368
lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/iframeembed.js
Normal file
368
lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/iframeembed.js
Normal file
@@ -0,0 +1,368 @@
|
||||
|
||||
import Templates from 'core/templates';
|
||||
import { getString } from 'core/str';
|
||||
import * as ModalEvents from 'core/modal_events';
|
||||
import { component } from './common';
|
||||
import IframeModal from './iframemodal';
|
||||
import Selectors from './selectors';
|
||||
import { getLti, getLaunchUrl, getMediaCMSUrl } from './options';
|
||||
|
||||
export default class IframeEmbed {
|
||||
editor = null;
|
||||
currentModal = null;
|
||||
isUpdating = false;
|
||||
selectedIframe = null;
|
||||
debounceTimer = null;
|
||||
iframeLibraryLoaded = false;
|
||||
selectedLibraryVideo = null;
|
||||
|
||||
constructor(editor) {
|
||||
this.editor = editor;
|
||||
}
|
||||
|
||||
parseInput(input) {
|
||||
if (!input || !input.trim()) {
|
||||
return null;
|
||||
}
|
||||
input = input.trim();
|
||||
|
||||
// Check for iframe
|
||||
const iframeMatch = input.match(/<iframe[^>]*src=["']([^"']+)["'][^>]*>/i);
|
||||
if (iframeMatch) {
|
||||
return this.parseEmbedUrl(iframeMatch[1]);
|
||||
}
|
||||
|
||||
// Check URL
|
||||
if (input.startsWith('http://') || input.startsWith('https://')) {
|
||||
return this.parseVideoUrl(input);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
parseVideoUrl(url) {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const baseUrl = `${urlObj.protocol}//${urlObj.host}`;
|
||||
|
||||
// Check if it matches configured MediaCMS URL (if strictly required)
|
||||
// For now we accept any valid MediaCMS-like structure
|
||||
|
||||
// /view?m=ID
|
||||
if (urlObj.pathname.includes('/view') && urlObj.searchParams.has('m')) {
|
||||
return {
|
||||
baseUrl: baseUrl,
|
||||
videoId: urlObj.searchParams.get('m'),
|
||||
isEmbed: false
|
||||
};
|
||||
}
|
||||
|
||||
// /embed?m=ID
|
||||
if (urlObj.pathname.includes('/embed') && urlObj.searchParams.has('m')) {
|
||||
return {
|
||||
baseUrl: baseUrl,
|
||||
videoId: urlObj.searchParams.get('m'),
|
||||
isEmbed: true,
|
||||
// Parse options
|
||||
showTitle: urlObj.searchParams.get('showTitle') === '1',
|
||||
linkTitle: urlObj.searchParams.get('linkTitle') === '1',
|
||||
showRelated: urlObj.searchParams.get('showRelated') === '1',
|
||||
showUserAvatar: urlObj.searchParams.get('showUserAvatar') === '1',
|
||||
};
|
||||
}
|
||||
|
||||
// Check if it's already a launch.php URL
|
||||
if (urlObj.pathname.includes('/filter/mediacms/launch.php') && urlObj.searchParams.has('token')) {
|
||||
return {
|
||||
baseUrl: baseUrl,
|
||||
videoId: urlObj.searchParams.get('token'),
|
||||
isEmbed: true,
|
||||
isLaunchUrl: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
baseUrl: baseUrl,
|
||||
rawUrl: url,
|
||||
isGeneric: true
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
parseEmbedUrl(url) {
|
||||
return this.parseVideoUrl(url);
|
||||
}
|
||||
|
||||
buildEmbedUrl(parsed, options) {
|
||||
if (parsed.isGeneric) {
|
||||
return parsed.rawUrl;
|
||||
}
|
||||
|
||||
const launchUrl = getLaunchUrl(this.editor);
|
||||
if (launchUrl && parsed.videoId) {
|
||||
const url = new URL(launchUrl);
|
||||
url.searchParams.set('token', parsed.videoId);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
// Fallback to direct embed if launchUrl missing
|
||||
const url = new URL(`${parsed.baseUrl}/embed`);
|
||||
url.searchParams.set('m', parsed.videoId);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
async getTemplateContext(data = {}) {
|
||||
return {
|
||||
elementid: this.editor.getElement().id,
|
||||
isupdating: this.isUpdating,
|
||||
url: data.url || '',
|
||||
showTitle: data.showTitle !== false,
|
||||
linkTitle: data.linkTitle !== false,
|
||||
showRelated: data.showRelated !== false,
|
||||
showUserAvatar: data.showUserAvatar !== false,
|
||||
responsive: data.responsive !== false,
|
||||
startAtEnabled: data.startAtEnabled || false,
|
||||
startAt: data.startAt || '0:00',
|
||||
width: data.width || 560,
|
||||
height: data.height || 315,
|
||||
is16_9: !data.aspectRatio || data.aspectRatio === '16:9',
|
||||
is4_3: data.aspectRatio === '4:3',
|
||||
is1_1: data.aspectRatio === '1:1',
|
||||
isCustom: data.aspectRatio === 'custom',
|
||||
};
|
||||
}
|
||||
|
||||
async displayDialogue() {
|
||||
this.selectedIframe = this.getSelectedIframe();
|
||||
const data = this.getCurrentIframeData();
|
||||
this.isUpdating = data !== null;
|
||||
this.iframeLibraryLoaded = false;
|
||||
|
||||
this.currentModal = await IframeModal.create({
|
||||
title: await getString('iframemodaltitle', component),
|
||||
templateContext: await this.getTemplateContext(data || {}),
|
||||
});
|
||||
|
||||
await this.registerEventListeners(this.currentModal);
|
||||
}
|
||||
|
||||
getSelectedIframe() {
|
||||
const node = this.editor.selection.getNode();
|
||||
if (node.nodeName.toLowerCase() === 'iframe') return node;
|
||||
return node.querySelector('iframe') || null;
|
||||
}
|
||||
|
||||
getCurrentIframeData() {
|
||||
if (!this.selectedIframe) return null;
|
||||
const src = this.selectedIframe.getAttribute('src');
|
||||
const parsed = this.parseInput(src);
|
||||
|
||||
return {
|
||||
url: src,
|
||||
width: this.selectedIframe.getAttribute('width') || 560,
|
||||
height: this.selectedIframe.getAttribute('height') || 315,
|
||||
showTitle: parsed?.showTitle ?? true,
|
||||
// ... other defaults
|
||||
};
|
||||
}
|
||||
|
||||
getFormValues(root) {
|
||||
const form = root.querySelector(Selectors.IFRAME.elements.form);
|
||||
// Helper to safely get value or checked state
|
||||
const getVal = (sel) => form.querySelector(sel)?.value;
|
||||
const getCheck = (sel) => form.querySelector(sel)?.checked;
|
||||
|
||||
return {
|
||||
url: getVal(Selectors.IFRAME.elements.url).trim(),
|
||||
showTitle: getCheck(Selectors.IFRAME.elements.showTitle),
|
||||
linkTitle: getCheck(Selectors.IFRAME.elements.linkTitle),
|
||||
showRelated: getCheck(Selectors.IFRAME.elements.showRelated),
|
||||
showUserAvatar: getCheck(Selectors.IFRAME.elements.showUserAvatar),
|
||||
responsive: getCheck(Selectors.IFRAME.elements.responsive),
|
||||
aspectRatio: getVal(Selectors.IFRAME.elements.aspectRatio),
|
||||
width: parseInt(getVal(Selectors.IFRAME.elements.width)) || 560,
|
||||
height: parseInt(getVal(Selectors.IFRAME.elements.height)) || 315,
|
||||
};
|
||||
}
|
||||
|
||||
async generateIframeHtml(values) {
|
||||
const parsed = this.parseInput(values.url);
|
||||
if (!parsed) return '';
|
||||
|
||||
const embedUrl = this.buildEmbedUrl(parsed, values);
|
||||
|
||||
const aspectRatioCalcs = {
|
||||
'16:9': '16 / 9',
|
||||
'4:3': '4 / 3',
|
||||
'1:1': '1 / 1',
|
||||
'custom': `${values.width} / ${values.height}`
|
||||
};
|
||||
|
||||
const context = {
|
||||
src: embedUrl,
|
||||
width: values.width,
|
||||
height: values.height,
|
||||
responsive: values.responsive,
|
||||
aspectRatioValue: aspectRatioCalcs[values.aspectRatio] || '16 / 9',
|
||||
};
|
||||
|
||||
const { html } = await Templates.renderForPromise(
|
||||
`${component}/iframe_embed_output`,
|
||||
context
|
||||
);
|
||||
return html;
|
||||
}
|
||||
|
||||
async updatePreview(root) {
|
||||
const values = this.getFormValues(root);
|
||||
const previewContainer = root.querySelector(Selectors.IFRAME.elements.preview);
|
||||
|
||||
if (!values.url) {
|
||||
previewContainer.innerHTML = '<span>Enter URL</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = this.parseInput(values.url);
|
||||
if (!parsed) {
|
||||
previewContainer.innerHTML = '<span class="text-danger">Invalid URL</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
const embedUrl = this.buildEmbedUrl(parsed, values);
|
||||
// Simple preview
|
||||
previewContainer.innerHTML = `<iframe src="${embedUrl}" width="100%" height="200" frameborder="0"></iframe>`;
|
||||
}
|
||||
|
||||
// ... Event listeners and Modal handling ...
|
||||
// Simplified for brevity, assuming standard handlers
|
||||
|
||||
async registerEventListeners(modal) {
|
||||
const root = modal.getRoot()[0];
|
||||
const form = root.querySelector(Selectors.IFRAME.elements.form);
|
||||
|
||||
// Input changes update preview
|
||||
form.addEventListener('change', () => this.updatePreview(root));
|
||||
form.querySelector(Selectors.IFRAME.elements.url).addEventListener('input', () => this.updatePreview(root));
|
||||
|
||||
// Tab switching
|
||||
const tabUrl = form.querySelector(Selectors.IFRAME.elements.tabUrlBtn);
|
||||
const tabLib = form.querySelector(Selectors.IFRAME.elements.tabIframeLibraryBtn);
|
||||
|
||||
if (tabLib) {
|
||||
tabLib.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.switchToTab(root, 'library');
|
||||
this.loadIframeLibrary(root);
|
||||
});
|
||||
}
|
||||
|
||||
if (tabUrl) {
|
||||
tabUrl.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.switchToTab(root, 'url');
|
||||
});
|
||||
}
|
||||
|
||||
modal.getRoot().on(ModalEvents.save, () => this.handleDialogueSubmission(modal));
|
||||
|
||||
// Listen for messages
|
||||
window.addEventListener('message', (e) => this.handleIframeLibraryMessage(root, e));
|
||||
}
|
||||
|
||||
switchToTab(root, tab) {
|
||||
const form = root.querySelector(Selectors.IFRAME.elements.form);
|
||||
const urlPane = form.querySelector(Selectors.IFRAME.elements.paneUrl);
|
||||
const libPane = form.querySelector(Selectors.IFRAME.elements.paneIframeLibrary);
|
||||
const urlBtn = form.querySelector(Selectors.IFRAME.elements.tabUrlBtn);
|
||||
const libBtn = form.querySelector(Selectors.IFRAME.elements.tabIframeLibraryBtn);
|
||||
|
||||
if (tab === 'url') {
|
||||
urlPane.classList.add('show', 'active');
|
||||
libPane.classList.remove('show', 'active');
|
||||
urlBtn.classList.add('active');
|
||||
libBtn.classList.remove('active');
|
||||
} else {
|
||||
urlPane.classList.remove('show', 'active');
|
||||
libPane.classList.add('show', 'active');
|
||||
urlBtn.classList.remove('active');
|
||||
libBtn.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
loadIframeLibrary(root) {
|
||||
const ltiConfig = getLti(this.editor);
|
||||
if (ltiConfig && ltiConfig.contentItemUrl) {
|
||||
const iframe = root.querySelector(Selectors.IFRAME.elements.iframeLibraryFrame);
|
||||
const loading = root.querySelector(Selectors.IFRAME.elements.iframeLibraryLoading);
|
||||
|
||||
if (iframe && !iframe.src) {
|
||||
loading.classList.remove('d-none');
|
||||
iframe.classList.add('d-none');
|
||||
|
||||
iframe.onload = () => {
|
||||
loading.classList.add('d-none');
|
||||
iframe.classList.remove('d-none');
|
||||
};
|
||||
|
||||
iframe.src = ltiConfig.contentItemUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleIframeLibraryMessage(root, event) {
|
||||
const data = event.data;
|
||||
if (!data) return;
|
||||
|
||||
let embedUrl = null;
|
||||
let videoId = null;
|
||||
|
||||
// LTI Deep Linking Response
|
||||
if (data.type === 'ltiDeepLinkingResponse' || data.messageType === 'LtiDeepLinkingResponse') {
|
||||
const items = data.content_items || data.contentItems || [];
|
||||
if (items.length) {
|
||||
embedUrl = items[0].url || items[0].embed_url;
|
||||
// Try to extract ID from URL if not provided
|
||||
// But actually we just need the ID to build the launch URL
|
||||
// If the response gives us a full embed URL, we can parse it
|
||||
if (embedUrl) {
|
||||
const parsed = this.parseInput(embedUrl);
|
||||
if (parsed) videoId = parsed.videoId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MediaCMS custom message
|
||||
if (data.action === 'selectMedia' || data.type === 'videoSelected') {
|
||||
embedUrl = data.embedUrl || data.url;
|
||||
videoId = data.mediaId || data.videoId || data.id;
|
||||
}
|
||||
|
||||
if (videoId) {
|
||||
// Populate URL field with the clean embed URL (launch URL) if possible,
|
||||
// or just the direct URL which parseInput will handle
|
||||
|
||||
// Actually, best to set the raw MediaCMS URL and let parseInput -> buildEmbedUrl handle the conversion
|
||||
// But if we have the ID, we can construct a view URL
|
||||
const mediaCMSUrl = getMediaCMSUrl(this.editor);
|
||||
if (mediaCMSUrl) {
|
||||
const viewUrl = `${mediaCMSUrl}/view?m=${videoId}`;
|
||||
const urlInput = root.querySelector(Selectors.IFRAME.elements.url);
|
||||
urlInput.value = viewUrl;
|
||||
this.updatePreview(root);
|
||||
this.switchToTab(root, 'url');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleDialogueSubmission(modal) {
|
||||
const root = modal.getRoot()[0];
|
||||
const values = this.getFormValues(root);
|
||||
const html = await this.generateIframeHtml(values);
|
||||
|
||||
if (html) {
|
||||
this.editor.insertContent(html);
|
||||
}
|
||||
modal.hide();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
import Modal from 'core/modal';
|
||||
import ModalRegistry from 'core/modal_registry';
|
||||
import { component } from './common';
|
||||
|
||||
export default class IframeModal extends Modal {
|
||||
static TYPE = `${component}/iframe_embed_modal`;
|
||||
static TEMPLATE = `${component}/iframe_embed_modal`;
|
||||
}
|
||||
|
||||
ModalRegistry.register(IframeModal.TYPE, IframeModal, IframeModal.TEMPLATE);
|
||||
25
lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/options.js
Normal file
25
lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/options.js
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
import { component } from './common';
|
||||
|
||||
export const register = (editor) => {
|
||||
const registerOption = editor.options.register;
|
||||
|
||||
registerOption(`${component}:mediacmsurl`, {
|
||||
processor: 'string',
|
||||
default: ''
|
||||
});
|
||||
|
||||
registerOption(`${component}:launchUrl`, {
|
||||
processor: 'string',
|
||||
default: ''
|
||||
});
|
||||
|
||||
registerOption(`${component}:lti`, {
|
||||
processor: 'object',
|
||||
default: {}
|
||||
});
|
||||
};
|
||||
|
||||
export const getMediaCMSUrl = (editor) => editor.options.get(`${component}:mediacmsurl`);
|
||||
export const getLaunchUrl = (editor) => editor.options.get(`${component}:launchUrl`);
|
||||
export const getLti = (editor) => editor.options.get(`${component}:lti`);
|
||||
23
lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/plugin.js
Normal file
23
lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/plugin.js
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
import { getTinyMCE } from 'editor_tiny/loader';
|
||||
import { getPluginMetadata } from 'editor_tiny/utils';
|
||||
import { component, pluginName } from './common';
|
||||
import * as Commands from './commands';
|
||||
import * as Options from './options';
|
||||
import * as Configuration from './configuration';
|
||||
|
||||
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) => {
|
||||
Options.register(editor);
|
||||
setupCommands(editor);
|
||||
return pluginMetadata;
|
||||
});
|
||||
|
||||
resolve([`${component}/plugin`, Configuration]);
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
export default {
|
||||
IFRAME: {
|
||||
elements: {
|
||||
form: '.tiny-mediacms-iframe-form',
|
||||
url: 'input[name="mediacms_url"]',
|
||||
width: 'input[name="mediacms_width"]',
|
||||
height: 'input[name="mediacms_height"]',
|
||||
preview: '.tiny-mediacms-preview',
|
||||
tabUrlBtn: '#tiny-mediacms-tab-url',
|
||||
tabIframeLibraryBtn: '#tiny-mediacms-tab-library',
|
||||
paneUrl: '#tiny-mediacms-pane-url',
|
||||
paneIframeLibrary: '#tiny-mediacms-pane-library',
|
||||
iframeLibraryFrame: '.tiny-mediacms-library-frame',
|
||||
iframeLibraryLoading: '.tiny-mediacms-library-loading',
|
||||
iframeLibraryPlaceholder: '.tiny-mediacms-library-placeholder'
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user