feat(exp360): add EXP360 module for embedding 360° content
Introduce the EXP360 activity module to embed interactive 360° content in Moodle. This update includes: - Database schema for the exp360 table. - Language support for the module. - Frontend form to create exp360 activities. - Script to handle 360° content embedding within a modal. - Two new PHP scripts to display and serve 360° content. - Version and plugin initialization. These changes allow users to seamlessly integrate and interact with 360° content within Moodle courses.
This commit is contained in:
parent
58993cbf51
commit
5b45d9ea82
8 changed files with 369 additions and 179 deletions
17
db/install.xml
Normal file
17
db/install.xml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<XMLDB PATH="mod/exp360/db" VERSION="2024070500" COMMENT="exp360 module">
|
||||||
|
<TABLES>
|
||||||
|
<TABLE NAME="exp360">
|
||||||
|
<FIELDS>
|
||||||
|
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" COMMENT="Primary key"/>
|
||||||
|
<FIELD NAME="course" TYPE="int" LENGTH="10" NOTNULL="true" COMMENT="Course ID"/>
|
||||||
|
<FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" COMMENT="Activity name"/>
|
||||||
|
<FIELD NAME="content_id" TYPE="char" LENGTH="255" NOTNULL="true" COMMENT="Content ID"/>
|
||||||
|
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" COMMENT="Last modified time"/>
|
||||||
|
</FIELDS>
|
||||||
|
<KEYS>
|
||||||
|
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||||
|
</KEYS>
|
||||||
|
</TABLE>
|
||||||
|
</TABLES>
|
||||||
|
</XMLDB>
|
10
lang/en/exp360.php
Normal file
10
lang/en/exp360.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$string['modulename'] = 'EXP360 Activity';
|
||||||
|
$string['modulenameplural'] = 'EXP360 Activities';
|
||||||
|
$string['modulename_help'] = 'Use the EXP360 module to embed interactive 360° content.';
|
||||||
|
$string['exp360:addinstance'] = 'Add a new EXP360 activity';
|
||||||
|
$string['exp360:submit'] = 'Submit EXP360 activity';
|
||||||
|
$string['exp360:view'] = 'View EXP360 activity';
|
||||||
|
$string['pluginadministration'] = 'EXP360 administration';
|
||||||
|
$string['pluginname'] = 'EXP360';
|
26
mod_form.php
Normal file
26
mod_form.php
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require_once("$CFG->dirroot/course/moodleform_mod.php");
|
||||||
|
|
||||||
|
class mod_exp360_mod_form extends moodleform_mod {
|
||||||
|
public function definition() {
|
||||||
|
$mform = $this->_form;
|
||||||
|
|
||||||
|
$mform->addElement('header', 'general', get_string('general', 'form'));
|
||||||
|
|
||||||
|
$mform->addElement('text', 'name', get_string('exp360name', 'exp360'), array('size' => '64'));
|
||||||
|
$mform->setType('name', PARAM_TEXT);
|
||||||
|
$mform->addRule('name', null, 'required', null, 'client');
|
||||||
|
$mform->addRule('name', null, 'maxlength', 255, 'client');
|
||||||
|
|
||||||
|
$this->standard_intro_elements();
|
||||||
|
|
||||||
|
$mform->addElement('text', 'content_id', get_string('contentid', 'exp360'), array('size' => '64'));
|
||||||
|
$mform->setType('content_id', PARAM_TEXT);
|
||||||
|
$mform->addRule('content_id', null, 'required', null, 'client');
|
||||||
|
|
||||||
|
$this->standard_coursemodule_elements();
|
||||||
|
|
||||||
|
$this->add_action_buttons();
|
||||||
|
}
|
||||||
|
}
|
40
script.js
40
script.js
|
@ -1,3 +1,4 @@
|
||||||
|
(function () {
|
||||||
function loadScript(url, integrity, crossorigin) {
|
function loadScript(url, integrity, crossorigin) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const script = document.createElement("script");
|
const script = document.createElement("script");
|
||||||
|
@ -11,16 +12,10 @@ function loadScript(url, integrity, crossorigin) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFeedbackLinks() {
|
function getFeedbackLinks() {
|
||||||
// Select all <a> elements with the class "stretched-link"
|
|
||||||
const links = document.querySelectorAll("a.stretched-link");
|
const links = document.querySelectorAll("a.stretched-link");
|
||||||
|
|
||||||
// Initialize an array to store the href attributes
|
|
||||||
const feedbackLinks = [];
|
const feedbackLinks = [];
|
||||||
|
|
||||||
// Iterate through the selected links
|
|
||||||
links.forEach((link) => {
|
links.forEach((link) => {
|
||||||
const href = link.getAttribute("href");
|
const href = link.getAttribute("href");
|
||||||
// Check if the href includes "/feedback/"
|
|
||||||
if (href) {
|
if (href) {
|
||||||
if (href.includes("/feedback/")) {
|
if (href.includes("/feedback/")) {
|
||||||
feedbackLinks.push(href);
|
feedbackLinks.push(href);
|
||||||
|
@ -29,7 +24,6 @@ function getFeedbackLinks() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return feedbackLinks;
|
return feedbackLinks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +62,7 @@ loadScript(
|
||||||
var scriptId = "script-" + Math.random().toString(36).substring(2, 9);
|
var scriptId = "script-" + Math.random().toString(36).substring(2, 9);
|
||||||
currentScript.id = scriptId;
|
currentScript.id = scriptId;
|
||||||
|
|
||||||
contentUrl = $(currentScript).attr("data-content-url");
|
var contentUrl = "/mod/exp360/view_content.php?id=" + $(currentScript).attr("data-activity-id");
|
||||||
|
|
||||||
function openInModal(url, block) {
|
function openInModal(url, block) {
|
||||||
$("#modal-iframe").attr("src", url);
|
$("#modal-iframe").attr("src", url);
|
||||||
|
@ -96,46 +90,34 @@ loadScript(
|
||||||
}
|
}
|
||||||
|
|
||||||
async function nextContent() {
|
async function nextContent() {
|
||||||
// Get the currently executing <script> element
|
|
||||||
var modal = $("#fullScreenModal");
|
var modal = $("#fullScreenModal");
|
||||||
var currentLi = $("#" + modal.attr("data-block"));
|
var currentLi = $("#" + modal.attr("data-block"));
|
||||||
|
|
||||||
// Find the next <li> sibling with the "section" class
|
|
||||||
var nextLi = currentLi.next("li.section");
|
var nextLi = currentLi.next("li.section");
|
||||||
|
|
||||||
// Find the <a> element with the "stretched-link" class within the next <li> element
|
|
||||||
var aLink = nextLi.find("a.stretched-link");
|
var aLink = nextLi.find("a.stretched-link");
|
||||||
|
|
||||||
if (aLink.length > 0) {
|
if (aLink.length > 0) {
|
||||||
// Get the "href" attribute of the <a> element
|
|
||||||
let hrefValue = aLink.attr("href");
|
let hrefValue = aLink.attr("href");
|
||||||
|
|
||||||
// Check if "feedback" is in the "href" attribute
|
|
||||||
if (hrefValue.includes("feedback")) {
|
if (hrefValue.includes("feedback")) {
|
||||||
// Replace "view" with "complete" in the "href" attribute
|
|
||||||
hrefValue = hrefValue.replace("view", "complete");
|
hrefValue = hrefValue.replace("view", "complete");
|
||||||
|
|
||||||
// Go to embed mode
|
|
||||||
hrefValue = hrefValue + "&embed=1";
|
hrefValue = hrefValue + "&embed=1";
|
||||||
} else if (hrefValue.includes("/quiz/")) {
|
} else if (hrefValue.includes("/quiz/")) {
|
||||||
// Replace "view" with "attempt" in the "href" attribute
|
|
||||||
hrefValue = hrefValue.replace("view", "attempt");
|
hrefValue = hrefValue.replace("view", "attempt");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Call URL to see where it redirects to
|
|
||||||
const response = await fetch(hrefValue);
|
const response = await fetch(hrefValue);
|
||||||
hrefValue = response.url;
|
hrefValue = response.url;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching the URL:', error);
|
console.error('Error fetching the URL:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go to embed mode
|
|
||||||
hrefValue = hrefValue + "&embed=1";
|
hrefValue = hrefValue + "&embed=1";
|
||||||
}
|
}
|
||||||
|
|
||||||
return openInModal(hrefValue, nextLi.attr("id"));
|
return openInModal(hrefValue, nextLi.attr("id"));
|
||||||
} else {
|
} else {
|
||||||
// Check if there is a button with the "btn-primary" class
|
|
||||||
var button = nextLi.find("button.btn-primary");
|
var button = nextLi.find("button.btn-primary");
|
||||||
|
|
||||||
if (button.length > 0) {
|
if (button.length > 0) {
|
||||||
|
@ -146,21 +128,15 @@ loadScript(
|
||||||
hideContent();
|
hideContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
nextActivity = nextContent;
|
|
||||||
window.top.nextContent = nextContent;
|
|
||||||
|
|
||||||
async function openQuizModal(number) {
|
async function openQuizModal(number) {
|
||||||
// Get the feedback link
|
|
||||||
let href = getFeedbackLinks()[number - 1];
|
let href = getFeedbackLinks()[number - 1];
|
||||||
|
|
||||||
if (href.includes("/feedback/")) {
|
if (href.includes("/feedback/")) {
|
||||||
href = href.replace("view", "complete");
|
href = href.replace("view", "complete");
|
||||||
} else if (href.includes("/quiz/")) {
|
} else if (href.includes("/quiz/")) {
|
||||||
// Replace "view" with "attempt" in the "href" attribute
|
|
||||||
href = href.replace("view", "attempt");
|
href = href.replace("view", "attempt");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Call URL to see where it redirects to
|
|
||||||
const response = await fetch(href);
|
const response = await fetch(href);
|
||||||
href = response.url;
|
href = response.url;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -170,13 +146,10 @@ loadScript(
|
||||||
console.log(href);
|
console.log(href);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go to embed mode
|
|
||||||
href = href + "&embed=1&modal=1";
|
href = href + "&embed=1&modal=1";
|
||||||
|
|
||||||
// Open in #regular-modal-iframe
|
|
||||||
$("#regular-modal-iframe").attr("src", href);
|
$("#regular-modal-iframe").attr("src", href);
|
||||||
|
|
||||||
// Show the modal
|
|
||||||
$("#regularModal").modal("show");
|
$("#regularModal").modal("show");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,17 +158,20 @@ loadScript(
|
||||||
$("#regular-modal-iframe").attr("src", "about:blank");
|
$("#regular-modal-iframe").attr("src", "about:blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attach global functions to window.top
|
||||||
|
window.top.nextContent = nextContent;
|
||||||
window.top.openQuizModal = openQuizModal;
|
window.top.openQuizModal = openQuizModal;
|
||||||
window.top.openModal = openQuizModal;
|
window.top.openModal = openQuizModal;
|
||||||
window.top.closeQuizModal = closeQuizModal;
|
window.top.closeQuizModal = closeQuizModal;
|
||||||
|
window.top.goToQuiz = window.top.nextContent;
|
||||||
|
|
||||||
|
|
||||||
$(currentScript)
|
$(currentScript)
|
||||||
.parent()
|
.parent()
|
||||||
.append(
|
.append(
|
||||||
`<button id="contentButton-` +
|
`<button id="contentButton-` +
|
||||||
scriptId +
|
scriptId +
|
||||||
`"class="btn btn-primary" type="button"
|
`" class="btn btn-primary" type="button">360° Content</button>`
|
||||||
> 360° Content </button>`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
var button = $("#contentButton-" + scriptId);
|
var button = $("#contentButton-" + scriptId);
|
||||||
|
@ -205,6 +181,7 @@ loadScript(
|
||||||
$(currentScript).closest("li.section").attr("id")
|
$(currentScript).closest("li.section").attr("id")
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("head").append(
|
$("head").append(
|
||||||
'<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">'
|
'<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">'
|
||||||
);
|
);
|
||||||
|
@ -212,3 +189,4 @@ loadScript(
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
});
|
});
|
||||||
|
})();
|
||||||
|
|
50
test.html
Normal file
50
test.html
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src='https://cdn.exp360.com/embed/js/exp360-embed-3.8.59.js?v=3&d=d667dff2-97bb-437e-85d2-f7550d87a4d0&p=web'></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function startContent() {
|
||||||
|
exp360.openContent('{1513cf9a-d385-4793-b97e-d7626ab6c12a}', { popup: false,autorotate:true,autorotate_speed:5,autorotate_delay:2500,teleport_animation:false });
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentUrl = window.location.href;
|
||||||
|
var url = new URL(currentUrl);
|
||||||
|
var searchParams = new URLSearchParams(url.search);
|
||||||
|
|
||||||
|
var params = {};
|
||||||
|
searchParams.forEach((value, key) => {
|
||||||
|
params[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
function initialize() {
|
||||||
|
// Create a new div element
|
||||||
|
var newDiv = document.createElement("div");
|
||||||
|
|
||||||
|
// Set the required attributes
|
||||||
|
newDiv.setAttribute("data-exp360-type", "player");
|
||||||
|
|
||||||
|
var contentId = params['contentid'] || '1513cf9a-d385-4793-b97e-d7626ab6c12a';
|
||||||
|
newDiv.setAttribute("data-exp360-id", contentId);
|
||||||
|
|
||||||
|
var windowWidth = document.documentElement.clientWidth;
|
||||||
|
var windowHeight = document.documentElement.clientHeight;
|
||||||
|
|
||||||
|
// Set the data-exp360-params attribute with the current window width and height
|
||||||
|
newDiv.setAttribute("data-exp360-params", `width=${windowWidth};height=${windowHeight};`);
|
||||||
|
|
||||||
|
// Append the new div to the body (or another desired parent element)
|
||||||
|
document.body.appendChild(newDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onload = initialize;
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
7
version.php
Normal file
7
version.php
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
defined('MOODLE_INTERNAL') || die();
|
||||||
|
|
||||||
|
$plugin->version = 2024070500; // The current module version (Date: YYYYMMDDXX)
|
||||||
|
$plugin->requires = 2021051700; // Requires this Moodle version
|
||||||
|
$plugin->component = 'mod_exp360'; // Full name of the plugin (used for diagnostics)
|
36
view.php
Normal file
36
view.php
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require_once(dirname(dirname(dirname(__FILE__))) . '/config.php');
|
||||||
|
require_once(dirname(__FILE__) . '/lib.php');
|
||||||
|
|
||||||
|
$id = optional_param('id', 0, PARAM_INT); // Course Module ID
|
||||||
|
$n = optional_param('n', 0, PARAM_INT); // exp360 instance ID
|
||||||
|
|
||||||
|
if ($id) {
|
||||||
|
$cm = get_coursemodule_from_id('exp360', $id, 0, false, MUST_EXIST);
|
||||||
|
$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST);
|
||||||
|
$exp360 = $DB->get_record('exp360', array('id' => $cm->instance), '*', MUST_EXIST);
|
||||||
|
} else if ($n) {
|
||||||
|
$exp360 = $DB->get_record('exp360', array('id' => $n), '*', MUST_EXIST);
|
||||||
|
$course = $DB->get_record('course', array('id' => $exp360->course), '*', MUST_EXIST);
|
||||||
|
$cm = get_coursemodule_from_instance('exp360', $exp360->id, $course->id, false, MUST_EXIST);
|
||||||
|
} else {
|
||||||
|
print_error('You must specify a course_module ID or an instance ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
require_login($course, true, $cm);
|
||||||
|
|
||||||
|
$PAGE->set_url('/mod/exp360/view.php', array('id' => $cm->id));
|
||||||
|
$PAGE->set_title(format_string($exp360->name));
|
||||||
|
$PAGE->set_heading(format_string($course->fullname));
|
||||||
|
|
||||||
|
echo $OUTPUT->header();
|
||||||
|
|
||||||
|
$content_id = $exp360->content_id;
|
||||||
|
|
||||||
|
echo "<script
|
||||||
|
data-activity-id='$content_id'
|
||||||
|
src='/mod/exp360/script.js'>
|
||||||
|
</script>";
|
||||||
|
|
||||||
|
echo $OUTPUT->footer();
|
66
view_content.php
Normal file
66
view_content.php
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require_once(dirname(dirname(dirname(__FILE__))) . '/config.php');
|
||||||
|
require_once(dirname(__FILE__) . '/lib.php');
|
||||||
|
|
||||||
|
$id = required_param('id', PARAM_INT); // Course Module ID
|
||||||
|
|
||||||
|
$cm = get_coursemodule_from_id('exp360', $id, 0, false, MUST_EXIST);
|
||||||
|
$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST);
|
||||||
|
$exp360 = $DB->get_record('exp360', array('id' => $cm->instance), '*', MUST_EXIST);
|
||||||
|
|
||||||
|
require_login($course, true, $cm);
|
||||||
|
|
||||||
|
$content_id = $exp360->content_id;
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<script src='https://cdn.exp360.com/embed/js/exp360-embed-3.8.59.js?v=3&d=d667dff2-97bb-437e-85d2-f7550d87a4d0&p=web'></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function startContent() {
|
||||||
|
exp360.openContent('<?php echo $content_id; ?>', {
|
||||||
|
popup: false,
|
||||||
|
autorotate: true,
|
||||||
|
autorotate_speed: 5,
|
||||||
|
autorotate_delay: 2500,
|
||||||
|
teleport_animation: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentUrl = window.location.href;
|
||||||
|
var url = new URL(currentUrl);
|
||||||
|
var searchParams = new URLSearchParams(url.search);
|
||||||
|
|
||||||
|
var params = {};
|
||||||
|
searchParams.forEach((value, key) => {
|
||||||
|
params[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
function initialize() {
|
||||||
|
var newDiv = document.createElement('div');
|
||||||
|
newDiv.setAttribute('data-exp360-type', 'player');
|
||||||
|
newDiv.setAttribute('data-exp360-id', '<?php echo $content_id; ?>');
|
||||||
|
var windowWidth = document.documentElement.clientWidth;
|
||||||
|
var windowHeight = document.documentElement.clientHeight;
|
||||||
|
newDiv.setAttribute('data-exp360-params', `width=${windowWidth};height=${windowHeight};`);
|
||||||
|
document.body.appendChild(newDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onload = initialize;
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
Loading…
Reference in a new issue