AutorÃa | Ultima modificación | Ver Log |
/**
* Amanote filter script.
*
* @copyright 2020 Amaplex Software
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/modal_factory'], ($, modalFactory) =>
{
class Main
{
/**
* Init the plugin's scripts. This method is called by AMD.
*
* @param rawUserParams
*/
public init(rawUserParams: string): void
{
// Parse the params.
const pluginParams = window['amanote_params'];
const moodleUserParams = Main.parseParams(rawUserParams);
if (!pluginParams || !moodleUserParams)
{
return;
}
// Init the Moodle service (Singleton).
MoodleService.init(pluginParams, moodleUserParams, modalFactory);
// Add Amanote button to each course modules.
const courseModuleFilter = new CourseModuleFilter();
courseModuleFilter.addButtonToCourseModules();
}
/**
* Parse the given plugin params.
*
* @param rawParams - The serialized params.
*
* @returns The parsed params as an object.
*/
private static parseParams(rawParams: string): IMoodleUserParams
{
try
{
return JSON.parse(rawParams);
}
catch (error)
{
console.error(error);
return null;
}
}
}
class MoodleService
{
private static instance: MoodleService;
constructor(private readonly pluginParams: IPluginParams,
private readonly moodleUserParams: IMoodleUserParams,
private readonly modalFactory: any)
{
if (MoodleService.instance)
{
throw new Error("Error - Use MoodleService.getInstance()");
}
try
{
const userOpeningMode = localStorage.getItem(StorageKeysEnum.OpeningMode);
if (userOpeningMode !== null)
{
this.pluginParams.plugin.openingMode = userOpeningMode as OpeningModeEnum;
}
}
catch (error)
{
console.log(error);
}
}
/**
* Get the plugin params.
*
* @returns The moodle plugin params.
*/
public getPluginParams(): IPluginParams
{
return this.pluginParams;
}
/**
* Get the user params.
*
* @returns The user params.
*/
public getUserParams(): IMoodleUserParams
{
return this.moodleUserParams;
}
/**
* Get the modal factory.
*
* @returns The modal factory.
*/
public getModalFactory(): any
{
return this.modalFactory;
}
/**
* Get an annotatable by course module id (cmid).
*
* @param cmid - The course module id (cmid).
*
* @returns The annotatable.
*/
public getAnnotatableByCmId(cmid: number): IAnnotatable
{
return (this.pluginParams.annotatables || [])
.filter(cm => cm.cmid == cmid).pop();
}
/**
* Get an annotatable by id.
*
* @param id - The annotatable id.
*
* @returns The annotatable.
*/
public getAnnotatableById(id: string): IAnnotatable
{
return (this.pluginParams.annotatables || [])
.filter(a => a.id == id).pop();
}
/**
* Get a annotatable by the path of the content.
*
* @param path - The path to find.
*
* @returns The annotatable.
*/
public getAnnotatableByContentPath(path: string): IAnnotatable
{
const annotatable = this.pluginParams.annotatables || [];
for (let i = 0; i < annotatable.length; i++)
{
if (annotatable[i].url === path)
{
return annotatable[i];
}
else if (annotatable[i].internal)
{
let path1 = annotatable[i].url;
let path2 = path.split('pluginfile.php')[1]
.split('?')[0]
.replace('intro/0', 'intro')
.replace('content/0/', 'content/1/');
path1 = decodeURIComponent(path1 || '');
path2 = decodeURIComponent(path2 || '');
if (path1 && path2 && path1 === path2)
{
return annotatable[i];
}
}
}
return null;
}
/**
* Get the filename of the note linked to the annotatable if any.
*
* @param annotatable The annotatable.
*
* @returns The file name if any or null otherwise.
*/
public getSavedNoteFilenameForAnnotatable(annotatable: IAnnotatable): string
{
const savedNotes = this.pluginParams.savedNotes || {};
if (savedNotes[annotatable.id + '.ama'])
{
return savedNotes[annotatable.id + '.ama'].filename;
}
else if (annotatable.legacyid && savedNotes[annotatable.legacyid + '.ama'])
{
return savedNotes[annotatable.legacyid + '.ama'].filename;
}
return null;
}
/**
* Get the Amanote logo for a given annotatable.
*
* @param annotatable The annotatable.
*
* @returns The logo depending if the annotatable is annotated or note.
*/
public getLogoForAnnotatable(annotatable: IAnnotatable): string
{
if (this.getSavedNoteFilenameForAnnotatable(annotatable))
{
return this.pluginParams.plugin.annotatedLogo;
}
return this.pluginParams.plugin.logo;
}
/**
* Generate an URL to open a file in Amanote.
*
* @param annotatable - The annotatable to open.
* @param route - The route.
*
* @returns The generated url.
*/
public generateAmanoteURL(annotatable: IAnnotatable, route = 'note-taking'): string
{
if (!annotatable)
{
return '';
}
if (route === 'note-taking' && this.pluginParams.plugin.target != OpeningTargetEnum.Amanote)
{
return `${this.pluginParams.siteURL}/filter/amanote/annotate.php?annotatableId=${annotatable.id}`;
}
// Parse the PDF path.
let filePath = annotatable.url;
if (annotatable.internal && filePath.indexOf('pluginfile.php') >= 0)
{
filePath = filePath.split('pluginfile.php')[1].replace('content/0/', 'content/1/');
}
else
{
filePath = encodeURIComponent(filePath);
}
// Generate the AMA path.
const noteFilename = this.getSavedNoteFilenameForAnnotatable(annotatable) || `${annotatable.id}.ama`;
const amaPath = this.pluginParams.privateFilePath + noteFilename;
let protocol = 'https';
if (this.pluginParams.siteURL.indexOf('https') < 0)
{
protocol = 'http';
}
if (route === 'note-taking' && annotatable.kind === ContentKindEnum.Video)
{
route = `/note-taking/moodle/video/${annotatable.id}`;
}
else
{
route = `/moodle/${route}`;
}
return protocol + '://app.amanote.com/' + this.pluginParams.language + route + '?' +
'siteURL=' + this.pluginParams.siteURL + '&' +
'accessToken=' + this.moodleUserParams.token.value + '&' +
'tokenExpDate=' + this.moodleUserParams.token.expiration + '&' +
'userId=' + this.moodleUserParams.id + '&' +
'filePath=' + filePath + '&' +
'mimeType=' + annotatable.mimetype + '&' +
'amaPath=' + amaPath + '&' +
'resourceId=' + annotatable.id + '&' +
'legacyResourceId=' + (annotatable.legacyid || annotatable.id) + '&' +
'saveInProvider=' + (this.pluginParams.plugin.saveInProvider ? '1' : '0') + '&' +
'providerVersion=' + this.pluginParams.moodle.version + '&' +
'pluginVersion=' + this.pluginParams.plugin.version + '&' +
'key=' + this.pluginParams.plugin.key + '&' +
'worksheet=' + (this.pluginParams.plugin.worksheet ? '1' : '0') + '&' +
'anonymous=' + (this.pluginParams.plugin.anonymous ? '1' : '0');
}
/**
* Init the Singleton.
*
* @param pluginParams - The plugin params.
* @param moodleUserParams - The moodle user params.
* @param modalFactory - THe modal factory.
*/
public static init(pluginParams: IPluginParams, moodleUserParams: IMoodleUserParams, modalFactory: any): void
{
MoodleService.instance = new MoodleService(pluginParams, moodleUserParams, modalFactory);
}
/**
* Get the Singleton instance.
*
* @returns The Singleton instance.
*/
public static getInstance(): MoodleService
{
return MoodleService.instance;
}
}
class CourseModuleFilter
{
private static readonly annotatableIDAttribute = 'annotatable-id';
private static readonly amanoteButtonClass = 'amanote-button';
private observer: MutationObserver;
private menuModal = new MenuModal();
private moodleService = MoodleService.getInstance();
private params = this.moodleService.getPluginParams();
private userParams = this.moodleService.getUserParams();
constructor() { }
/**
* Add the Amanote buttons on each supported module and listen changes.
*/
public addButtonToCourseModules(): void
{
this.addAmanoteButtons();
if (this.observer)
{
this.observer.disconnect();
}
this.observer = new MutationObserver((mutationsList) =>
{
if (this.doesMutationsContainAnActivity(mutationsList))
{
this.addAmanoteButtons();
}
});
const targetNode = document.getElementById('page-content');
this.observer.observe(targetNode, { childList: true, subtree: true });
}
/**
* Check if a given DOM mutations list contains a new activity instance node.
*
* @param mutationsList - The list of mutations.
*
* @returns True if a mutation contains a new activity instance, False otherwise.
*/
private doesMutationsContainAnActivity(mutationsList: any[]): boolean
{
for (let i = 0; i < mutationsList.length; i++)
{
const mutation = mutationsList[i];
if (mutation.type !== 'childList')
{
continue;
}
for (let j = 0; j < mutation.addedNodes.length; j++)
{
const addedNode = mutation.addedNodes[j];
if ($(addedNode).find('.activityinstance, .activity-instance').length > 0)
{
return true;
}
}
}
return false;
}
/**
* Add the Amanote buttons on each supported module.
*/
private addAmanoteButtons(): void
{
this.forEachNewInstances(['modtype_resource', 'modtype_url'], (element) =>
{
const annotatable = this.getAnnotatableFromElement(element);
if (!annotatable)
{
return;
}
let activityLink = $(element).find('.activitytitle').find('a').first();
if (activityLink.length === 0)
{
activityLink = $(element).find('a').first();
}
if (this.openWithButton())
{
const button = this.generateAmanoteButton(annotatable);
activityLink.css('display', 'inline-block');
activityLink.removeClass('stretched-link');
if ($(element).find('.activity-instance').length > 0)
{
$(element).find('.activity-instance').find('.activityname').children().first().after(button);
}
else if ($(element).find('.activity-basis').length > 0)
{
$(element).find('.activity-basis').first().children().children().first().after(button);
}
else
{
$(element).find('.activityinstance').first().children().first().after(button);
}
}
else
{
this.replaceLink(activityLink, annotatable);
// Replace link on activity icon if any.
const iconLink = $(element).find('a.activity-icon').first();
if (iconLink.length > 0)
{
this.replaceLink(iconLink, annotatable);
}
}
this.addOnDeleteWarning($(element));
});
this.forEachNewInstances(['fp-filename-icon'], (element) =>
{
// Get file id from file url.
const fileLink = $(element).find('a').first();
if (fileLink.length !== 1)
{
return;
}
const filePath = fileLink.attr('href');
const annotatable = this.moodleService.getAnnotatableByContentPath(filePath);
if (!annotatable)
{
return;
}
if (this.openWithButton())
{
const button = this.generateAmanoteButton(annotatable);
fileLink.css('display', 'inline-block');
fileLink.after(button);
}
else
{
this.replaceLink(fileLink, annotatable);
}
});
this.forEachNewInstances(['modtype_folder'], (element) =>
{
this.addOnDeleteWarning($(element));
});
this.forEachNewInstances(['modtype_label'], (element) =>
{
const annotatable = this.getAnnotatableFromElement(element);
if (!annotatable)
{
return;
}
if (this.openWithButton())
{
const button = this.generateAmanoteButton(annotatable);
$(element).find('.mediaplugin').first().children().children().first().after(button);
}
else
{
this.replaceLink($(element).find('a').first(), annotatable);
}
this.addOnDeleteWarning($(element));
});
// Listen click on amanote button.
setTimeout(() =>
{
$(`.${CourseModuleFilter.amanoteButtonClass}`).on('click', (event) =>
{
event.preventDefault();
const annotatableId = $(event.currentTarget).attr(CourseModuleFilter.annotatableIDAttribute);
const annotatable = this.moodleService.getAnnotatableById(annotatableId);
if (this.params.plugin.openingMode === OpeningModeEnum.FileClick)
{
annotatable.openInMoodleURL = event.currentTarget.href;
}
if ((this.params.plugin.openingMode !== OpeningModeEnum.FileClick || this.params.plugin.preventDownload) && !this.userParams.isTeacher)
{
window.open(this.moodleService.generateAmanoteURL(annotatable, 'note-taking'), 'blank');
return;
}
this.menuModal.open(annotatable);
});
}, 500);
}
/**
* Get the annotatable from an element. The element must contains and id property with
* a value of kind "module-xx" where xx is the cmid.
*
* @param element - The element.
*
* @returns The annotatable corresponding to the cmid of the element.
*/
private getAnnotatableFromElement(element: HTMLElement): IAnnotatable
{
const elementId = $(element).attr('id');
if (!elementId || elementId.indexOf('module-') < 0)
{
return;
}
const courseModuleId = parseInt(elementId.replace('module-', ''), 10);
return this.moodleService.getAnnotatableByCmId(courseModuleId);
}
/**
* Determine if it should open the menu with a button or by replacing the current link.
*
* @returns True the menu should open with a button.
*/
private openWithButton(): boolean
{
return this.params.plugin.openingMode !== OpeningModeEnum.FileClick;
}
/**
* Apply action to all new course module instance.
*
* @param classNames - The module class names.
* @param action - The action.
*/
private forEachNewInstances(classNames: string[], action: (e: HTMLElement) => void): void
{
$('.' + classNames.join(', .')).each((index, element) =>
{
if ($(element).find('.' + CourseModuleFilter.amanoteButtonClass).length > 0)
{
return;
}
action(element);
});
}
/**
* Generate a new button for a given annotatable.
*
* @param annotatable - The annotatable for which the button should be created.
*
* @returns The JQuery generate button.
*/
private generateAmanoteButton(annotatable: IAnnotatable): JQuery
{
const moodleService = MoodleService.getInstance();
const logo = moodleService.getLogoForAnnotatable(annotatable);
const widthByMode = {
[OpeningModeEnum.FileClick]: 90,
[OpeningModeEnum.LogoNextToFile]: 90,
[OpeningModeEnum.IconNextToFile]: 40,
[OpeningModeEnum.IconNextToFileWithText]: 130,
};
const width = widthByMode[this.params.plugin.openingMode] || 90;
const a = $(`<a class="mx-4 my-2 amanote-button"><img src="${logo}" width="${width}px" alt="Open in Amanote"></a>`);
a.css('display', 'inline-block');
a.css('cursor', 'pointer');
a.css('margin', 'auto');
if (this.params.plugin.openingMode === OpeningModeEnum.LogoNextToFile)
{
a.css('min-width', '110px');
}
a.attr(CourseModuleFilter.annotatableIDAttribute, annotatable.id);
return a;
}
/**
* Replace a link element with an Amanote button.
*
* @param link - The link to replace.
* @param annotatable - The annotatable for which the button should be created.
*/
private replaceLink(link: JQuery, annotatable: IAnnotatable): void
{
link.attr(CourseModuleFilter.annotatableIDAttribute, annotatable.id);
link.addClass('amanote-button');
link.css('cursor', 'pointer');
}
/**
* Add a warning when the user want to delete a resource.
*
* @param activity - The activity to which the warning should be added.
*/
private addOnDeleteWarning(activity: JQuery): void
{
activity.find('.editing_delete').first().on('click', () =>
{
setTimeout(() => { this.menuModal.showDeleteWarning(); }, 500);
});
}
}
class MenuModal
{
private moodleService = MoodleService.getInstance();
private modalFactory = this.moodleService.getModalFactory();
private pluginParams = this.moodleService.getPluginParams();
private moodleUserParams = this.moodleService.getUserParams();
constructor() { }
/**
* Open the menu modal for a given annotatable.
*/
public open(annotatable: IAnnotatable): Promise<void>
{
if (!annotatable)
{
return;
}
const modalParams = {
title: 'Amanote',
body: this.generateModalBodyHTML(annotatable),
footer: '',
};
return this.modalFactory.create(modalParams)
.then((modal) =>
{
modal.show();
});
}
/**
* Show a warning popup before deleting a resource.
*/
public showDeleteWarning(): void
{
const hideDeleteWarningExp = localStorage.getItem(StorageKeysEnum.HideDeleteWarningExp);
if (hideDeleteWarningExp && !isNaN(Date.parse(hideDeleteWarningExp)))
{
const hideDate = new Date(hideDeleteWarningExp);
const now = new Date();
if ((now.getTime() - hideDate.getTime()) < (30 * 24 * 60 * 60 * 1000))
{
return;
}
}
const guideLink = 'https://help.amanote.com/en/support/solutions/articles/36000448676';
var message = `<div class="alert alert-warning">
<strong>Warning</strong>
<p>${this.pluginParams.strings.deletefilewarning}</p>
<p><a href="${guideLink}" target="_blank">${this.pluginParams.strings.seeguide}</a></p>
</div>
<div style="text-align: center; margin-top: 1rem;">
<a class="text-muted" style="cursor: pointer;" data-action="hide"
onclick="localStorage.setItem('${StorageKeysEnum.HideDeleteWarningExp}', new Date().toISOString());">
${this.pluginParams.strings.stopmodal}
</a>
</div>`;
const modalParams = {
title: 'Amanote Warning',
body: message,
footer: '',
};
this.modalFactory.create(modalParams)
.then((modal) =>
{
modal.show();
});
}
/**
* Generate the modal body for a given annotatable.
*
* @param annotatable - The annotatable.
*
* @returns The modal body in HTML.
*/
private generateModalBodyHTML(annotatable: IAnnotatable): string
{
const openInAmanoteURL = this.moodleService.generateAmanoteURL(annotatable, 'note-taking');
let body = `<p class="mb-0 text-muted">${this.pluginParams.strings.modalDescription}</p>`;
body += MenuModal.generateButtonHTML(openInAmanoteURL, this.pluginParams.strings.annotateResource, 'fa fa-edit', 'font-weight: 600; background: #2cdf90; color: #03341f; border: none');
if (this.pluginParams.plugin.openingMode === OpeningModeEnum.FileClick && !this.pluginParams.plugin.preventDownload)
{
body += MenuModal.generateButtonHTML(annotatable.openInMoodleURL, this.pluginParams.strings.viewResource, 'fa fa-eye', 'font-weight: 600;');
}
if (this.moodleUserParams.isTeacher && annotatable.kind === ContentKindEnum.Document)
{
body += '<hr style="margin-bottom: 0px">';
// Add Learning Analytics.
const openAnalyticsURL = this.moodleService.generateAmanoteURL(annotatable, `document-analytics/${annotatable.id}/view`);
body += MenuModal.generateButtonHTML(openAnalyticsURL, this.pluginParams.strings.openAnalytics);
// Add Podcast Creator.
if (this.pluginParams.plugin.key && !this.pluginParams.plugin.anonymous)
{
const openPodcastCreatorURL = this.moodleService.generateAmanoteURL(annotatable, 'podcast/creator');
body += MenuModal.generateButtonHTML(openPodcastCreatorURL, this.pluginParams.strings.openPodcastCreator);
}
// Add Open student's works.
if (this.pluginParams.plugin.worksheet && !this.pluginParams.plugin.anonymous)
{
const openStudentWorkURL = this.moodleService.generateAmanoteURL(annotatable, `document-analytics/${annotatable.id}/notes`);
body += MenuModal.generateButtonHTML(openStudentWorkURL, this.pluginParams.strings.openStudentsWorks);
}
}
if (this.pluginParams.plugin.openingMode === OpeningModeEnum.FileClick &&
!this.moodleUserParams.isTeacher &&
!this.pluginParams.plugin.preventDownload)
{
body += `
<div style="text-align: center; margin-top: 1rem;">
<a class="text-muted" style="cursor: pointer;" data-action="hide"
onclick="localStorage.setItem('${StorageKeysEnum.OpeningMode}', '${OpeningModeEnum.LogoNextToFile}'); location.reload()">
${this.pluginParams.strings.stopmodal}
</a>
</div>`;
}
return body;
}
/**
* Generate a button.
*
* @param href - The button's href.
* @param title - The button's title.
*
* @returns The button as HTML string.
*/
private static generateButtonHTML(href: string, title: string, faIconClass?: string, style: string = ''): string
{
let faIconHTML = '';
if (faIconClass)
{
faIconHTML = `<i class="${faIconClass} mr-2"></i> `;
}
return `<a class="btn btn-secondary" style="width: 100%; margin-top: 1rem; ${style}" href="${href}" target="_blank">${faIconHTML}${title}</a>`;
}
}
return new Main();
});
enum OpeningModeEnum
{
FileClick = '0',
LogoNextToFile = '1',
IconNextToFile = '2',
IconNextToFileWithText = '3',
}
enum OpeningTargetEnum
{
Amanote = '0',
MoodleFullscreen = '1',
MoodleEmbedded = '2',
}
enum ContentKindEnum
{
Document = 'document',
Video = 'video',
}
enum StorageKeysEnum
{
OpeningMode = 'amanote.preferences.openingMode',
HideDeleteWarningExp = 'amanote.preferences.hideDeleteWarningExp',
}
interface IAnnotatable
{
id: string;
legacyid: string;
cmid: number;
mimetype: string;
url: string;
internal: boolean;
kind: ContentKindEnum;
openInMoodleURL: string;
}
interface IMoodleUserParams
{
id: string;
token: {
value: string;
expiration: number;
},
isTeacher: boolean;
}
interface IPluginSettings
{
version: string;
saveInProvider: boolean;
openingMode: OpeningModeEnum;
target: OpeningTargetEnum;
preventDownload: boolean;
anonymous: boolean;
key: string;
logo: string;
annotatedLogo: string;
worksheet: boolean;
}
interface IPluginParams
{
siteURL: string;
language: string;
privateFilePath: string;
annotatables: IAnnotatable[];
savedNotes: { [filename: string]: { filename: string}};
moodle: {
version: string;
}
plugin: IPluginSettings,
strings: {
modalDescription: string;
annotateResource: string;
viewResource: string;
downloadNotes: string;
openAnalytics: string;
openPodcastCreator: string;
openStudentsWorks: string;
teacher: string;
deletefilewarning: string;
seeguide: string;
stopmodal: string;
}
}