@caspingus/lt
Version:
A utility library of helpers and tools for working with Learnosity APIs.
266 lines (253 loc) • 10.4 kB
JavaScript
import * as ssmlEditor from '@caspingus/ssml-editor/src/index';
/**
* Extensions add specific functionality to Learnosity APIs.
* They rely on modules within LT being available.
*
* --
*
* Adds an SSML editor which can be launched from a custom
* Question Editor button in the toolbar.
*
* You MUST use the object as defined below. Also, note the `toolbar_settings`
* which is required to place the custom button(s) where you want in the toolbar.
*
* ```
* {
* "config": {
* "dependencies": {
* "question_editor_api": {
* "init_options": {
* "rich_text_editor": {
* "customButtons": [
* {
* "func": "LT.extensions.ssmlEditor.launchSsmlEditor",
* "icon": "https://raw.githubusercontent.com/michaelsharman/LT/refs/heads/main/src/authoring/extensions/ui/ssmlEditor/assets/images/icon_tts.svg",
* "label": "Add SSML",
* "name": "addSsml",
* "attributes": ["content","stimulus","template","options"]
* }
* ],
* "toolbar_settings": {
* "ltr_toolbar": [
* {
* "items": ["Bold","Italic","Underline","-","TextColor","-", "LrnUnderlinedIndicator","-","RemoveFormat","FontSize"],
* "name": "basicstyles"
* },
* {
* "items": ["NumberedList","BulletedList","-","Indent","Outdent"],
* "name": "list"
* },
* {
* "items": ["JustifyLeft","JustifyCenter","JustifyRight","JustifyBlock"],
* "name": "justify"
* },
* {
* "items": ["Link","Unlink"],
* "name": "link"
* },
* {
* "items": ["Image","LrnMath","Table","Blockquote","SpecialChar"],
* "name": "insert"
* },
* {
* "items": ["LrnSimpleFeature"],
* "name": "simplefeature"
* },
* {
* "items": ["LrnResource"],
* "name": "resource"
* },
* {
* "items": ["LrnEditAriaLabel","LrnPopupContent"],
* "name": "editAriaLabel"
* },
* {
* "name": "custombuttons"
* },
* {
* "items": ["Undo","Redo"],
* "name": "clipboard"
* },
* {
* "items": ["Styles"],
* "name": "style"
* },
* {
* "items": ["Sourcedialog"],
* "name": "mode"
* },
* {
* "items": ["lrn_datatable"],
* "name": "data"
* }
* ]
* }
* }
* }
* }
* }
* }
* }
* ```
*
* <p><img src="https://raw.githubusercontent.com/michaelsharman/LT/main/src/assets/images/ssmleditor.png" alt="" width="660"></p>
* @module Extensions/Authoring/ssmlEditor
*/
const state = {
renderedCss: false,
};
/**
* Extension constructor.
* @example
* import { LT } from '@caspingus/lt/src/authoring/index';
*
* LT.init(authorApp); // Set up LT with the Author API application instance variable
* LT.extensions.ssmlEditor.run();
* @since 2.8.0
*/
export function run() {
state.renderedCss || injectCSS();
}
/**
* Called via a custom button in the rich text toolbar.
* Renders a modal with an SSML editor.
* @param {string} attribute Which Question Editor attribute is being edited
* @param {*} callback Called to return to the parent rich-text editor
* @since 2.8.0
*/
export function launchSsmlEditor(attribute, callback) {
const currentSelectedText = window.getSelection().toString();
const rangeSelectedHTML = window.getSelection().getRangeAt(0);
const container = document.createElement('div');
container.appendChild(rangeSelectedHTML.cloneContents());
const currentSelectedHTML = container.innerHTML;
const templateSsmlEditor = `
<div class="lrn-qe lrn-qe-modal" style="display: block;" id="lt__ssmlModalWrapper">
<div class="lrn-qe-ui">
<div class="lrn-qe-modal-dialog">
<div class="lrn-qe-modal-dialog-inner">
<div class="lrn-qe-modal-header">
<div class="lrn-qe-form-label lrn-qe-h4 lrn-qe-section-header">
<h4 class="lrn-qe-heading"><label class="lrn-qe-label lrn-qe-form-label-name">Enter SSML</label></h4>
</div>
<button type="button" class="lrn-qe-btn lrn-qe-modal-btn-close" aria-label="Close" tabindex="0">
<span class="lrn-qe-sr-only">Close</span>
<span aria-role="presentation" class="lrn-qe-i-cross"></span>
</button>
</div>
<div data-lrn-qe-selector="modal-outlet">
<div class="lrn-qe-modal-content" data-lrn-qe-modal-section="content">
<div id="editor"></div>
<div id="ssmlStatus"></div>
</div>
<div class="lrn-qe-modal-footer">
<ul class="lrn-qe-ul">
<li class="lrn-qe-li lrn-qe-modal-footer-item">
<button type="button" class="lrn-qe-btn lrn-qe-btn-default"><span>Cancel</span></button>
</li>
<li class="lrn-qe-li lrn-qe-modal-footer-item">
<button type="button" class="lrn-qe-btn lrn-qe-btn-primary lt__ssml-add" data-lrn-qe-modal-action="confirm"><span>Add SSML</span></button>
</li>
<li class="lrn-qe-li lrn-qe-modal-footer-item">
<button type="button" class="lrn-qe-btn lrn-qe-btn-primary lt__ssml-generate-audio"><span>⭐ Generate audio</span></button>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
`;
document.querySelector('.learnosity-question-editor').insertAdjacentHTML('beforeEnd', templateSsmlEditor);
const elClose = [];
elClose.push(document.querySelector('#lt__ssmlModalWrapper .lrn-qe-btn-default'));
elClose.push(document.querySelector('#lt__ssmlModalWrapper .lrn-qe-modal-btn-close'));
for (let i = 0; i < elClose.length; i++) {
elClose[i].addEventListener('click', () => {
removeElement('lt__ssmlModalWrapper');
delete window.quill;
return callback(currentSelectedHTML);
});
}
const elAdd = document.querySelector('#lt__ssmlModalWrapper .lt__ssml-add');
elAdd.addEventListener('click', () => {
removeElement('lt__ssmlModalWrapper');
const ssml = window.quill.getText();
delete window.quill;
return callback(`<span>${ssml}</span>`);
});
if (!window.hasOwnProperty('quill')) {
const quill = ssmlEditor.run('editor');
if (currentSelectedText) {
const cursorLocation = quill.getSelection();
quill.insertText(cursorLocation.index, currentSelectedText);
}
window.quill = quill;
window.ssmlEditor = ssmlEditor;
}
}
/**
* Removes an element from the DOM
* @param {string} id Id of the element to remove from the DOM
* @since 2.8.0
* @ignore
*/
function removeElement(id) {
document.getElementById(id).remove();
}
/**
* Injects the necessary CSS to the header
* @since 2.8.0
* @ignore
*/
function injectCSS() {
const elStyle = document.createElement('style');
const css = `
/* Learnosity SSML TTS styles */
#lt__ssmlModalWrapper .lrn-qe-modal-content {
height: 23em;
}
#lt__ssmlModalWrapper .ql-editor {
height: 12rem;
}
#lt__ssmlModalWrapper .ql-editor p {
margin: 0;
}
#lt__ssmlModalWrapper .ql-toolbar.ql-snow .ql-formats {
margin-right: 14px;
}
.hidden {
display: none;
}
#lt__ssmlModalWrapper .material-symbols-outlined {
font-size: 1.3rem;
position: relative;
top: -2px;
}
#lt__ssmlModalWrapper .ql-picker {
padding: 3px 0;
}
#lt__ssmlModalWrapper .ql-picker-label {
width: 45px;
height: 18px;
}
#ssmlStatus .material-symbols-outlined {
vertical-align: middle;
}
.ssmlStatusValid {
padding: 1em;
background-color: #cbf5d3;
}
.ssmlStatusInvalid {
padding: 1em;
background-color: #ffb9b9;
}
speak s {
text-decoration: none;
}
`;
elStyle.textContent = css;
document.head.append(elStyle);
state.renderedCss = true;
}