Current Path : C:/xampp/htdocs/moodle/mod/hvp/editor/scripts/ |
Current File : C:/xampp/htdocs/moodle/mod/hvp/editor/scripts/h5peditor-html.js |
/* global ns CKEDITOR */ /** * Adds a html text field to the form. * * @param {type} parent * @param {type} field * @param {type} params * @param {type} setValue * @returns {undefined} */ ns.Html = function (parent, field, params, setValue) { this.parent = parent; this.field = field; this.value = params; this.setValue = setValue; this.tags = ns.$.merge(['br'], (this.field.tags || this.defaultTags)); }; ns.Html.first = true; ns.Html.prototype.defaultTags = ['strong', 'em', 'del', 'h2', 'h3', 'a', 'ul', 'ol', 'table', 'hr']; // This should probably be named "hasTag()" instead... // And might be more efficient if this.tags.contains() were used? ns.Html.prototype.inTags = function (value) { return (ns.$.inArray(value.toLowerCase(), this.tags) >= 0); }; /** * Check if the provided button is enabled by config. * * @param {string} value * @return {boolean} */ ns.Html.prototype.inButtons = function (button) { return (H5PIntegration.editor !== undefined && H5PIntegration.editor.wysiwygButtons !== undefined && H5PIntegration.editor.wysiwygButtons.indexOf(button) !== -1); }; ns.Html.prototype.createToolbar = function () { var basicstyles = []; var paragraph = []; var formats = []; var inserts = []; var toolbar = []; // Basic styles if (this.inTags("strong") || this.inTags("b")) { basicstyles.push('Bold'); // Might make "strong" duplicated in the tag lists. Which doesn't really // matter. Note: CKeditor will only make strongs. this.tags.push("strong"); } if (this.inTags("em") || this.inTags("i")) { basicstyles.push('Italic'); // Might make "em" duplicated in the tag lists. Which again // doesn't really matter. Note: CKeditor will only make ems. this.tags.push("em"); } if (this.inTags("u")) basicstyles.push('Underline'); if (this.inTags("strike") || this.inTags("del") || this.inTags("s")) { basicstyles.push('Strike'); // Might make "strike" or "del" or both duplicated in the tag lists. Which // again doesn't really matter. this.tags.push("strike"); this.tags.push("del"); this.tags.push("s"); } if (this.inTags("sub")) basicstyles.push("Subscript"); if (this.inTags("sup")) basicstyles.push("Superscript"); if (basicstyles.length > 0) { basicstyles.push("-"); basicstyles.push("RemoveFormat"); toolbar.push({ name: 'basicstyles', items: basicstyles }); } // Alignment is added to all wysiwygs toolbar.push({ name: "justify", items: ["JustifyLeft", "JustifyCenter", "JustifyRight"] }); // Paragraph styles if (this.inTags("ul")) { paragraph.push("BulletedList"); this.tags.push("li"); } if (this.inTags("ol")) { paragraph.push("NumberedList"); this.tags.push("li"); } if (this.inTags("blockquote")) paragraph.push("Blockquote"); if (paragraph.length > 0) { toolbar.push(paragraph); } // Links. if (this.inTags("a")) { var items = ["Link", "Unlink"]; if (this.inTags("anchor")) { items.push("Anchor"); } toolbar.push({ name: "links", items: items }); } // Inserts if (this.inTags("img")) inserts.push("Image"); if (this.inTags("table")) { inserts.push("Table"); ns.$.merge(this.tags, ["tr", "td", "th", "colgroup", "thead", "tbody", "tfoot"]); } if (this.inTags("hr")) inserts.push("HorizontalRule"); if (this.inTags('code')) { if (this.inButtons('inlineCode')) { inserts.push('Code'); } if (this.inTags('pre') && this.inButtons('codeSnippet')) { inserts.push('CodeSnippet'); } } if (inserts.length > 0) { toolbar.push({ name: "insert", items: inserts }); } // Create wrapper for text styling options var styles = { name: "styles", items: [] }; var colors = { name: "colors", items: [] }; // Add format group if formatters in tags (h1, h2, etc). Formats use their // own format_tags to filter available formats. if (this.inTags("h1")) formats.push("h1"); if (this.inTags("h2")) formats.push("h2"); if (this.inTags("h3")) formats.push("h3"); if (this.inTags("h4")) formats.push("h4"); if (this.inTags("h5")) formats.push("h5"); if (this.inTags("h6")) formats.push("h6"); if (this.inTags("address")) formats.push("address"); if (this.inTags("pre")) formats.push("pre"); if (formats.length > 0 || this.inTags('p') || this.inTags('div')) { formats.push("p"); // If the formats are shown, always have a paragraph.. this.tags.push("p"); styles.items.push('Format'); } var ret = { toolbar: toolbar }; if (this.field.font !== undefined) { this.tags.push('span'); /** * Help set specified values for property. * * @private * @param {Array} values list * @param {string} prop Property * @param {string} [defProp] Default property name */ var setValues = function (values, prop, defProp) { ret[prop] = ''; for (var i = 0; i < values.length; i++) { var val = values[i]; if (val.label && val.css) { // Add label and CSS ret[prop] += val.label + '/' + val.css + ';'; // Check if default value if (defProp && val.default) { ret[defProp] = val.label; } } } }; /** * @private * @param {Array} values * @returns {string} */ var getColors = function (values) { var colors = ''; for (var i = 0; i < values.length; i++) { var val = values[i]; if (val.label && val.css) { var css = val.css.match(/^#?([a-f0-9]{3}[a-f0-9]{3}?)$/i); if (!css) { continue; } // Add label and CSS if (colors) { colors += ','; } colors += val.label + '/' + css[1]; } } return colors; }; if (this.field.font.family) { // Font family chooser styles.items.push('Font'); if (this.field.font.family instanceof Array) { // Use specified families setValues(this.field.font.family, 'font_names', 'font_defaultLabel'); } } if (this.field.font.size) { // Font size chooser styles.items.push('FontSize'); ret.fontSize_sizes = ''; if (this.field.font.size instanceof Array) { // Use specified sizes setValues(this.field.font.size, 'fontSize_sizes', 'fontSize_defaultLabel'); } else { ret.fontSize_defaultLabel = '100%'; // Standard font sizes that is available. var defaultAvailable = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72]; for (var i = 0; i < defaultAvailable.length; i++) { // Calculate percentage of standard font size. This enables scaling // in content types without rounding errors across browsers. var em = defaultAvailable[i] / 16; ret.fontSize_sizes += (em * 100) + '%/' + em + 'em;'; } } } if (this.field.font.color) { // Text color chooser colors.items.push('TextColor'); if (this.field.font.color instanceof Array) { ret.colorButton_colors = getColors(this.field.font.color); ret.colorButton_enableMore = false; } } if (this.field.font.background) { // Text background color chooser colors.items.push('BGColor'); if (this.field.font.background instanceof Array) { ret.colorButton_colors = getColors(this.field.font.color); ret.colorButton_enableMore = false; } } } // Add the text styling options if (styles.items.length) { toolbar.push(styles); } if (colors.items.length) { toolbar.push(colors); } // Set format_tags if not empty. CKeditor does not like empty format_tags. if (formats.length) { ret.format_tags = formats.join(';'); } // Enable selection of enterMode in module semantics. if (this.field.enterMode === 'p' || formats.length > 0) { this.tags.push('p'); ret.enterMode = CKEDITOR.ENTER_P; } else { // Default to DIV, not allowing BR at all. this.tags.push('div'); ret.enterMode = CKEDITOR.ENTER_DIV; } return ret; }; /** * Append field to wrapper. * * @param {type} $wrapper * @returns {undefined} */ ns.Html.prototype.appendTo = function ($wrapper) { var that = this; this.$item = ns.$(this.createHtml()).appendTo($wrapper); this.$input = this.$item.children('.ckeditor'); this.$errors = this.$item.children('.h5p-errors'); ns.bindImportantDescriptionEvents(this, this.field.name, this.parent); var ckConfig = { extraPlugins: "", startupFocus: true, enterMode: CKEDITOR.ENTER_DIV, allowedContent: true, // Disables the ckeditor content filter, might consider using it later... Must make sure it doesn't remove math... protectedSource: [], contentsCss: ns.basePath + 'styles/css/cke-contents.css', // We want to customize the CSS inside the editor codeSnippet_codeClass: 'h5p-hl' }; ns.$.extend(ckConfig, this.createToolbar()); // Look for additions in HtmlAddons if (ns.HtmlAddons) { for (var tag in ns.HtmlAddons) { if (that.inTags(tag)) { for (var provider in ns.HtmlAddons[tag]) { ns.HtmlAddons[tag][provider](ckConfig, that.tags); } } } } this.$item.children('.ckeditor').focus(function () { // Blur is not fired on destroy. Therefore we need to keep track of it! var blurFired = false; // Remove placeholder that.$placeholder = that.$item.find('.h5peditor-ckeditor-placeholder').detach(); if (ns.Html.first) { CKEDITOR.basePath = ns.basePath + '/ckeditor/'; } if (ns.Html.current === that) { return; } // Remove existing CK instance. ns.Html.removeWysiwyg(); CKEDITOR.document.getBody = function () { // Have to attach to an element that does not get hidden or removed, since an internal "calculator" element // inside CKeditor relies on this element to always exist and not be hidden. return new CKEDITOR.dom.element(window.document.body); }; // Override convertToPx to make sure the calculator is always visible so it can make the measurements and is // removed after the measurements are done, adapted from: // https://github.com/ckeditor/ckeditor4/blob/cae20318d46745cc46c811da4e7d68b38ca32449/core/tools.js#L899-L929 CKEDITOR.tools.convertToPx = function (cssLength) { const calculator = CKEDITOR.dom.element.createFromHtml( '<div style="position:absolute;left:-9999px;' + 'top:-9999px;margin:0px;padding:0px;border:0px;"' + '></div>', CKEDITOR.document ); CKEDITOR.document.getBody().append(calculator); if ( !( /%$/ ).test( cssLength ) ) { var isNegative = parseFloat( cssLength ) < 0, ret; if ( isNegative ) { cssLength = cssLength.replace( '-', '' ); } calculator.setStyle( 'width', cssLength ); ret = calculator.$.clientWidth; if (calculator.$ && calculator.$.parentNode) { calculator.$.parentNode.removeChild(calculator.$); } if ( isNegative ) { return -ret; } return ret; } if (calculator.$ && calculator.$.parentNode) { calculator.$.parentNode.removeChild(calculator.$); } return cssLength; }; ns.Html.current = that; ckConfig.width = this.offsetWidth - 8; // Avoid miscalculations that.ckeditor = CKEDITOR.replace(this, ckConfig); that.ckeditor.on('focus', function () { blurFired = false; }); that.ckeditor.once('destroy', function () { // In some cases, the blur event is not fired. Need to be sure it is, so that // validation and saving is done if (!blurFired) { blur(); } // Display placeholder if: // -- The value held by the field is empty AND // -- The value shown in the UI is empty AND // -- A placeholder is defined var value = that.ckeditor !== undefined ? that.ckeditor.getData() : that.$input.html(); if (that.$placeholder.length !== 0 && (value === undefined || value.length === 0) && (that.value === undefined || that.value.length === 0)) { that.$placeholder.appendTo(that.$item.find('.ckeditor')); } }); var blur = function () { blurFired = true; // Do not validate if the field has been hidden. if (that.$item.is(':visible')) { that.validate(); } }; that.ckeditor.on('blur', blur); // Add events to ckeditor. It is beeing done here since we know it exists // at this point... Use case from commit message: "Make the default // linkTargetType blank for ckeditor" - STGW if (ns.Html.first) { CKEDITOR.on('dialogDefinition', function (e) { // Take the dialog name and its definition from the event data. var dialogName = e.data.name; var dialogDefinition = e.data.definition; // Check if the definition is from the dialog window you are interested in (the "Link" dialog window). if (dialogName === 'link') { // Get a reference to the "Link Info" tab. var targetTab = dialogDefinition.getContents('target'); // Set the default value for the URL field. var urlField = targetTab.get('linkTargetType'); urlField['default'] = '_blank'; } // Override show event handler var onShow = dialogDefinition.onShow; dialogDefinition.onShow = function () { if (onShow !== undefined) { onShow.apply(this, arguments); } // Grab current item var $item = ns.Html.current.$item; // Position dialog above text field var itemPos = $item[0].getBoundingClientRect(); var dialogSize = this.getSize(); var x = itemPos.x + (itemPos.width / 2) - (dialogSize.width / 2); var y = itemPos.y + (itemPos.height / 2) - (dialogSize.height / 2); this.move(x, y, true); }; }); ns.Html.first = false; } }); }; /** * Create HTML for the HTML field. */ ns.Html.prototype.createHtml = function () { const id = ns.getNextFieldId(this.field); var input = '<div id="' + id + '"'; if (this.field.description !== undefined) { input += ' aria-describedby="' + ns.getDescriptionId(id) + '"'; } input += ' class="ckeditor" tabindex="0" contenteditable="true">'; if (this.value !== undefined) { input += this.value; } else if (this.field.placeholder !== undefined) { input += '<span class="h5peditor-ckeditor-placeholder">' + this.field.placeholder + '</span>'; } input += '</div>'; return ns.createFieldMarkup(this.field, ns.createImportantDescription(this.field.important) + input, id); }; /** * Validate the current text field. */ ns.Html.prototype.validate = function () { var that = this; if (that.$errors.children().length) { that.$errors.empty(); this.$input.addClass('error'); } // Get contents from editor var value = this.ckeditor !== undefined ? this.ckeditor.getData() : this.$input.html(); value = value // Remove placeholder text if any: .replace(/<span class="h5peditor-ckeditor-placeholder">.*<\/span>/, '') // Workaround for Microsoft browsers that otherwise can produce non-emtpy fields causing trouble .replace(/^<br>$/, ''); var $value = ns.$('<div>' + value + '</div>'); var textValue = $value.text(); // Check if we have any text at all. if (!this.field.optional && !textValue.length) { // We can accept empty text, if there's an image instead. if (! (this.inTags("img") && $value.find('img').length > 0)) { this.$errors.append(ns.createError(ns.t('core', 'requiredProperty', {':property': ns.t('core', 'textField')}))); } } // Verify HTML tags. Removes tags not in allowed tags. Will replace with // the tag's content. So if we get an unallowed container, the contents // will remain, without the container. $value.find('*').each(function () { if (! that.inTags(this.tagName)) { ns.$(this).replaceWith(ns.$(this).contents()); } }); value = $value.html(); // Display errors and bail if set. if (that.$errors.children().length) { return false; } else { this.$input.removeClass('error'); } this.value = value; this.setValue(this.field, value); this.$input.change(); // Trigger change event. return value; }; /** * Destroy H5PEditor existing CK instance. If it exists. */ ns.Html.removeWysiwyg = function () { if (ns.Html.current !== undefined) { try { ns.Html.current.ckeditor.destroy(); } catch (e) { // No-op, just stop error from propagating. This usually occurs if // the CKEditor DOM has been removed together with other DOM data. } ns.Html.current = undefined; } }; /** * Remove this item. */ ns.Html.prototype.remove = function () { this.$item.remove(); }; /** * When someone from the outside wants to set a value. * * @param {string} value */ ns.Html.prototype.forceValue = function (value) { if (this.ckeditor === undefined) { this.$input.html(value); } else { this.ckeditor.setData(value); } this.validate(); }; ns.widgets.html = ns.Html;