diff --git a/CHANGELOG.md b/CHANGELOG.md index bcbf88b2e1..bf8b682e2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,15 @@ - Froze `lib/deprecators/*.rb` constants that were Strings - Updated places that were incorrectly using keyword args. See [this article](https://makandracards.com/makandra/496481-changes-to-positional-and-keyword-args-in-ruby-3-0) for an overview +#### Upgraded TinyMCE to v6 + +- Upgraded TinyMCE to v6 (v5 EOL is April 20 2023) +- Adjusted JS code to conform to new TinyMCE version +- Adjusted views to work with the new version +- Updated variables.scss file to fix issue with button text/background color contrast +- Updated blocks/_tables.scss to fix issue with dropdown menu overlap against table +- updated config/initializers/assets.rb to copy over the tinymce skins and bootstrap glyphicons to the public directory so that they are accessible by TinyMCE and Bootstrap code + #### Removed webpacker gem As Webpacker is no longer maintained by the Rails community, we have replaced it by `jsbundling-rails` and `cssbundling-rails` for the Javascript & CSS compilation. diff --git a/app/assets/stylesheets/blocks/_buttons.scss b/app/assets/stylesheets/blocks/_buttons.scss index f137fc9cd0..ffc1d5ee69 100644 --- a/app/assets/stylesheets/blocks/_buttons.scss +++ b/app/assets/stylesheets/blocks/_buttons.scss @@ -15,6 +15,7 @@ .btn-primary.active:hover { background-color: $color-primary-background; border-color: $color-border-default; + color: $color-primary-text; outline: none; margin-top: 5px; margin-bottom: 10px; @@ -30,6 +31,7 @@ .btn-default.active:hover { background-color: $color-primary-background; border-color: $color-border-default; + color: $color-primary-text; outline: none; margin-top: 5px; margin-bottom: 10px; diff --git a/app/javascript/src/answers/edit.js b/app/javascript/src/answers/edit.js index 7376e40a4d..9dbbdd6b7b 100644 --- a/app/javascript/src/answers/edit.js +++ b/app/javascript/src/answers/edit.js @@ -3,195 +3,204 @@ import { isNumber, isString, } from '../utils/isType'; -import { Tinymce } from '../utils/tinymce.js.erb'; +import { Tinymce } from '../utils/tinymce.js'; import debounce from '../utils/debounce'; import { updateSectionProgress, getQuestionDiv } from '../utils/sectionUpdate'; import datePicker from '../utils/datePicker'; import TimeagoFactory from '../utils/timeagoFactory.js.erb'; $(() => { - const editorClass = 'tinymce_answer'; - const showSavingMessage = (jQuery) => jQuery.closest('.question-form').find('[data-status="saving"]').show(); - const hideSavingMessage = (jQuery) => jQuery.closest('.question-form').find('[data-status="saving"]').hide(); - const closestErrorSavingMessage = (jQuery) => jQuery.closest('.question-form').find('[data-status="error-saving"]'); - const questionId = (jQuery) => jQuery.closest('.form-answer').attr('data-autosave'); - const isStale = (jQuery) => jQuery.closest('.question-form').find('.answer-locking').text().trim().length !== 0; - const isReadOnly = () => $('.form-answer fieldset:disabled').length > 0; - const showOrHideQuestions = (data) => { - data.section_data.forEach((section) => { - updateSectionProgress(section.sec_id, section.no_ans, section.no_qns); - }); - data.qn_data.to_hide.forEach((questionid) => { - getQuestionDiv(questionid).slideUp(); - }); - data.qn_data.to_show.forEach((questionid) => { - getQuestionDiv(questionid).slideDown(); - }); - }; - /* - * A map of debounced functions, one for each input, textarea or select change at any - * form with class form-answer. The key represents a question id and the value holds - * the debounced function for a given input, textarea or select. Note, this map is - * populated on demand, i.e. the first time a change is made at a given input, textarea - * or select within the form, a new key-value should be created. Succesive times, the - * debounced function should be retrieved instead. - */ - const debounceMap = {}; - const autoSaving = (jQuery) => { - if (!isStale(jQuery)) { - jQuery.closest('.form-answer').trigger('submit'); - } - }; - const doneCallback = (data, jQuery) => { - const form = jQuery.closest('form'); - // Validation for the data object received - if (isObject(data)) { - if (isObject(data.question)) { // Object related to question within data received - if (isNumber(data.question.id)) { - if (isString(data.question.answer_status)) { - $(`#answer-status-${data.question.id}`).html(data.question.answer_status); - TimeagoFactory.render($('time.timeago')); - } - if (isString(data.question.locking)) { // When an answer is stale... - // Removes event handlers for the saved form - detachEventHandlers(form); // eslint-disable-line no-use-before-define - // Reflesh form view with the new partial form received - $(`#answer-form-${data.question.id}`).html(data.question.form); - // Retrieves the newly form added to the DOM - const newForm = $(`#answer-form-${data.question.id}`).find('form'); - // Attaches event handlers for the new form - attachEventHandlers(newForm); // eslint-disable-line no-use-before-define - // Refresh optimistic locking view with the form that caused the locking - $(`#answer-locking-${data.question.id}`).html(data.question.locking); - } else { // When answer is NOT stale... - $(`#answer-locking-${data.question.id}`).html(''); - if (isNumber(data.question.answer_lock_version)) { - form.find('#answer_lock_version').val(data.question.answer_lock_version); + if ($('form.form-answer').length > 0) { + const editorClass = 'tinymce_answer'; + const showSavingMessage = (jQuery) => jQuery.closest('.question-form').find('[data-status="saving"]').show(); + const hideSavingMessage = (jQuery) => jQuery.closest('.question-form').find('[data-status="saving"]').hide(); + const closestErrorSavingMessage = (jQuery) => jQuery.closest('.question-form').find('[data-status="error-saving"]'); + const questionId = (jQuery) => jQuery.closest('.form-answer').attr('data-autosave'); + const isStale = (jQuery) => jQuery.closest('.question-form').find('.answer-locking').text().trim().length !== 0; + const isReadOnly = () => $('.form-answer fieldset:disabled').length > 0; + const showOrHideQuestions = (data) => { + data.section_data.forEach((section) => { + updateSectionProgress(section.sec_id, section.no_ans, section.no_qns); + }); + data.qn_data.to_hide.forEach((questionid) => { + getQuestionDiv(questionid).slideUp(); + }); + data.qn_data.to_show.forEach((questionid) => { + getQuestionDiv(questionid).slideDown(); + }); + }; + /* + * A map of debounced functions, one for each input, textarea or select change at any + * form with class form-answer. The key represents a question id and the value holds + * the debounced function for a given input, textarea or select. Note, this map is + * populated on demand, i.e. the first time a change is made at a given input, textarea + * or select within the form, a new key-value should be created. Succesive times, the + * debounced function should be retrieved instead. + */ + const debounceMap = {}; + const autoSaving = (jQuery) => { + if (!isStale(jQuery)) { + jQuery.closest('.form-answer').trigger('submit'); + } + }; + const doneCallback = (data, jQuery) => { + const form = jQuery.closest('form'); + // Validation for the data object received + if (isObject(data)) { + if (isObject(data.question)) { // Object related to question within data received + if (isNumber(data.question.id)) { + if (isString(data.question.answer_status)) { + $(`#answer-status-${data.question.id}`).html(data.question.answer_status); + TimeagoFactory.render($('time.timeago')); + } + if (isString(data.question.locking)) { // When an answer is stale... + // Removes event handlers for the saved form + detachEventHandlers(form); // eslint-disable-line no-use-before-define + // Reflesh form view with the new partial form received + $(`#answer-form-${data.question.id}`).html(data.question.form); + // Retrieves the newly form added to the DOM + const newForm = $(`#answer-form-${data.question.id}`).find('form'); + // Attaches event handlers for the new form + attachEventHandlers(newForm); // eslint-disable-line no-use-before-define + // Refresh optimistic locking view with the form that caused the locking + $(`#answer-locking-${data.question.id}`).html(data.question.locking); + } else { // When answer is NOT stale... + $(`#answer-locking-${data.question.id}`).html(''); + if (isNumber(data.question.answer_lock_version)) { + form.find('#answer_lock_version').val(data.question.answer_lock_version); + } } } + }// End Object related to question within data received + if (isObject(data.plan)) { // Object related to plan within data received + if (isString(data.plan.progress)) { + $('.progress').html(data.plan.progress); + } } - }// End Object related to question within data received - if (isObject(data.plan)) { // Object related to plan within data received - if (isString(data.plan.progress)) { - $('.progress').html(data.plan.progress); - } + showOrHideQuestions(data); } - showOrHideQuestions(data); - } - }; - const failCallback = (error, jQuery) => { - closestErrorSavingMessage(jQuery).html( - (isObject(error.responseJSON) && isString(error.responseJSON.detail)) - ? error.responseJSON.detail : error.statusText, - ).show(); - }; - const changeHandler = (e) => { - const target = $(e.target); - const id = questionId(target); - if (!debounceMap[id]) { - debounceMap[id] = debounce(autoSaving); - } - debounceMap[id](target); - }; - const submitHandler = (e) => { - e.preventDefault(); - const target = $(e.target); - const form = target.closest('form'); - const id = questionId(target); - if (debounceMap[id]) { - // Cancels the delated execution of autoSaving - // (e.g. user clicks the button before the delay is met) - debounceMap[id].cancel(); - } - $.ajax({ - method: form.attr('method'), - url: form.attr('action'), - data: form.serializeArray(), - beforeSend: () => { - showSavingMessage(target); - }, - complete: () => { - hideSavingMessage(target); - }, - }).done((data) => { - doneCallback(data, target); - }).fail((error) => { - failCallback(error, target); - }); - }; - const blurHandler = (editor) => { - const target = $(editor.getElement()); - const id = questionId(target); - if (editor.isDirty()) { - editor.save(); // Saves contents from editor to the textarea element + }; + const failCallback = (error, jQuery) => { + closestErrorSavingMessage(jQuery).html( + (isObject(error.responseJSON) && isString(error.responseJSON.detail)) + ? error.responseJSON.detail : error.statusText, + ).show(); + }; + const changeHandler = (e) => { + const target = $(e.target); + const id = questionId(target); if (!debounceMap[id]) { debounceMap[id] = debounce(autoSaving); } debounceMap[id](target); - } - }; - const focusHandler = (editor) => { - const id = questionId($(editor.getElement())); - if (debounceMap[id]) { - /* Cancels the delayed execution of autoSaving, either because user - * transitioned from an option_based question to the comment or - * because the target element triggered blur and focus before - * the delayed execution of autoSaving. - */ - debounceMap[id].cancel(); - } - }; - const formHandlers = ({ jQuery, attachment = 'off' }) => { - // Listeners to change and submit for a form - jQuery[attachment]('change', changeHandler); - jQuery[attachment]('submit', submitHandler); - }; - const editorHandlers = (editor) => { - // Listeners to blur and focus events for a tinymce instance - editor.on('Blur', () => blurHandler(editor)); - editor.on('Focus', () => focusHandler(editor)); - }; + }; + const submitHandler = (e) => { + e.preventDefault(); + const target = $(e.target); + const form = target.closest('form'); + const id = questionId(target); + +console.log('SUBMITTING'); +console.log(target); +console.log(form); +console.log(id); + + if (debounceMap[id]) { + // Cancels the delated execution of autoSaving + // (e.g. user clicks the button before the delay is met) + debounceMap[id].cancel(); + } + $.ajax({ + method: form.attr('method'), + url: form.attr('action'), + data: form.serializeArray(), + beforeSend: () => { + showSavingMessage(target); + }, + complete: () => { + hideSavingMessage(target); + }, + }).done((data) => { + doneCallback(data, target); + }).fail((error) => { + failCallback(error, target); + }); + }; + const blurHandler = (editor) => { + const target = $(editor.getElement()); + const id = questionId(target); + if (editor.isDirty()) { + editor.save(); // Saves contents from editor to the textarea element + if (!debounceMap[id]) { + debounceMap[id] = debounce(autoSaving); + } + debounceMap[id](target); + } + }; + const focusHandler = (editor) => { + const id = questionId($(editor.getElement())); + if (debounceMap[id]) { + /* Cancels the delayed execution of autoSaving, either because user + * transitioned from an option_based question to the comment or + * because the target element triggered blur and focus before + * the delayed execution of autoSaving. + */ + debounceMap[id].cancel(); + } + }; + const formHandlers = ({ jQuery, attachment = 'off' }) => { + // Listeners to change and submit for a form + jQuery[attachment]('change', changeHandler); + jQuery[attachment]('submit', submitHandler); + }; + const editorHandlers = (editor) => { + // Listeners to blur and focus events for a tinymce instance + editor.on('Blur', () => blurHandler(editor)); + editor.on('Focus', () => focusHandler(editor)); + }; + /* + Detaches events from a specific form including its tinymce editor + @param { objecg } - jQueryForm to remove events + */ + const detachEventHandlers = (jQueryForm) => { + formHandlers({ jQuery: jQueryForm, attachment: 'off' }); + const tinymceId = jQueryForm.find(`.${editorClass}`).attr('id'); + Tinymce.destroyEditorById(tinymceId); + }; /* - Detaches events from a specific form including its tinymce editor - @param { objecg } - jQueryForm to remove events - */ - const detachEventHandlers = (jQueryForm) => { - formHandlers({ jQuery: jQueryForm, attachment: 'off' }); - const tinymceId = jQueryForm.find(`.${editorClass}`).attr('id'); - Tinymce.destroyEditorById(tinymceId); - }; - /* - Attaches events for a specific form including its tinymce editor - @param { objecg } - jQueryForm to add events - */ - const attachEventHandlers = (jQueryForm) => { - formHandlers({ jQuery: jQueryForm, attachment: 'on' }); - const tinymceId = jQueryForm.find(`.${editorClass}`).attr('id'); - Tinymce.init({ selector: `#${tinymceId}` }); - editorHandlers(Tinymce.findEditorById(tinymceId)); - }; - // Initial load - TimeagoFactory.render($('time.timeago')); - Tinymce.init({ selector: `.${editorClass}` }); - if (!isReadOnly()) { - // Attaches form and tinymce event handlers - Tinymce.findEditorsByClassName(editorClass).forEach(editorHandlers); - formHandlers({ jQuery: $('.form-answer'), attachment: 'on' }); - } else { - // Sets the editor mode for each editor to readonly - Tinymce.findEditorsByClassName(editorClass).forEach((editor) => { - editor.mode.set('readonly'); - }); - } + Attaches events for a specific form including its tinymce editor + @param { objecg } - jQueryForm to add events + */ + const attachEventHandlers = (jQueryForm) => { + formHandlers({ jQuery: jQueryForm, attachment: 'on' }); + const tinymceId = jQueryForm.find(`.${editorClass}`).attr('id'); + Tinymce.init({ selector: `#${tinymceId}` }); + editorHandlers(Tinymce.findEditorById(tinymceId)); + }; + // Initial load + TimeagoFactory.render($('time.timeago')); + Tinymce.init({ selector: `.${editorClass}` }); - datePicker(); + if (!isReadOnly()) { + // Attaches form and tinymce event handlers + Tinymce.findEditorsByClassName(editorClass).forEach(editorHandlers); + formHandlers({ jQuery: $('.form-answer'), attachment: 'on' }); + } else { + // Sets the editor mode for each editor to readonly + Tinymce.findEditorsByClassName(editorClass).forEach((editor) => { + editor.mode.set('readonly'); + }); + } - // Clicking the 'Comments & Guidance' div should toggle the guidance & comments section - $(document).on('click', '.toggle-guidance-section', (e) => { - const target = $(e.currentTarget); - target.parents('.question-body').find('.guidance-section').toggle(); - target.find('span.fa-chevron-right, span.fa-chevron-left') - .toggleClass('fa-chevron-right') - .toggleClass('fa-chevron-left'); - }); + datePicker(); + + // Clicking the 'Comments & Guidance' div should toggle the guidance & comments section + $(document).on('click', '.toggle-guidance-section', (e) => { + const target = $(e.currentTarget); + target.parents('.question-body').find('.guidance-section').toggle(); + target.find('span.fa-chevron-right, span.fa-chevron-left') + .toggleClass('fa-chevron-right') + .toggleClass('fa-chevron-left'); + }); + } }); diff --git a/app/javascript/src/guidances/newEdit.js b/app/javascript/src/guidances/newEdit.js index e5da657c8b..f018eca0b8 100644 --- a/app/javascript/src/guidances/newEdit.js +++ b/app/javascript/src/guidances/newEdit.js @@ -1,4 +1,4 @@ -import { Tinymce } from '../utils/tinymce.js.erb'; +import { Tinymce } from '../utils/tinymce.js'; $(() => { Tinymce.init({ selector: '#guidance_text' }); diff --git a/app/javascript/src/notes/index.js b/app/javascript/src/notes/index.js index e877f36469..c8f6e475af 100644 --- a/app/javascript/src/notes/index.js +++ b/app/javascript/src/notes/index.js @@ -1,4 +1,4 @@ -import { Tinymce } from '../utils/tinymce.js.erb'; +import { Tinymce } from '../utils/tinymce.js'; import { isObject, isString } from '../utils/isType'; import TimeagoFactory from '../utils/timeagoFactory.js.erb'; @@ -160,7 +160,9 @@ $(() => { $('.archive_note button[type="button"]')[attachment]('click', noteCancelHandler); }; const initOrReload = () => { - Tinymce.init({ selector: '.note' }); + $('.note').each((_idx, el) => { + Tinymce.init({ selector: `#${$(el).attr('id')}` }); + }); eventHandlers({ attachment: 'on' }); TimeagoFactory.render($('time.timeago')); }; diff --git a/app/javascript/src/orgAdmin/phases/newEdit.js b/app/javascript/src/orgAdmin/phases/newEdit.js index 6014e59952..db8486e939 100644 --- a/app/javascript/src/orgAdmin/phases/newEdit.js +++ b/app/javascript/src/orgAdmin/phases/newEdit.js @@ -1,5 +1,5 @@ // import 'bootstrap-sass/assets/javascripts/bootstrap/collapse'; -import { Tinymce } from '../../utils/tinymce.js.erb'; +import { Tinymce } from '../../utils/tinymce.js'; import { isObject, isString } from '../../utils/isType'; import getConstant from '../../utils/constants'; import { addAsterisks } from '../../utils/requiredField'; @@ -9,7 +9,7 @@ import initQuestionOption from '../questionOptions/index'; import updateConditions from '../conditions/updateConditions'; $(() => { - Tinymce.init({ selector: '.phase' }); + Tinymce.init({ selector: '#phase_description' }); const parentSelector = '.section-group'; const initQuestion = (context) => { @@ -61,36 +61,29 @@ $(() => { // For some reason the toolbar options are retained after the call to Tinymce.init() on // the views/notifications/edit.js file. Tried 'Object.assign' instead of '$.extend' but it // made no difference + const prefix = 'collapseSection' + let sectionId = selector; + if (sectionId.startsWith(prefix)) { + sectionId = `sc_${sectionId.replace(prefix, '')}_section_description` + } + Tinymce.init({ - selector: `${selector} .section`, - init_instance_callback(editor) { + selector: `#${sectionId}`, + init_instance_callback: (editor) => { // When the text editor changes to blank, set the corresponding destroy // field to true (if present). - editor.on('Change', () => { - const $texteditor = $(editor.targetElm); + editor.on('Change', (ed) => { + const $texteditor = $(ed.getContentAreaContainer()); const $fieldset = $texteditor.parents('fieldset'); const $hiddenField = $fieldset.find('input[type=hidden][id$="_destroy"]'); - $hiddenField.val(editor.getContent() === ''); + $hiddenField.val(ed.getContent() === ''); }); }, }); + const questionForm = $(selector).find('.question_form'); if (questionForm.length > 0) { - // Load Tinymce when the 'show' form has a question form. - // ONLY applicable for template customizations - Tinymce.init({ - selector: `${selector} .question_form .question`, - init_instance_callback(editor) { - // When the text editor changes to blank, set the corresponding destroy - // field to true (if present). - editor.on('Change', () => { - const $texteditor = $(editor.targetElm); - const $fieldset = $texteditor.parents('fieldset'); - const $hiddenField = $fieldset.find('input[type=hidden][id$="_destroy"]'); - $hiddenField.val(editor.getContent() === ''); - }); - }, - }); + initQuestion(selector); } } }; @@ -108,8 +101,9 @@ $(() => { // Display the section's html panelBody.attr('data-loaded', 'true'); panelBody.html(e.detail[0].html); + // Wire up the section - initSection(`#${panel.attr('id')}`); + initSection(`${panel.attr('id')}`); } }); @@ -158,8 +152,9 @@ $(() => { // Handle the section that has focus on initial page load const currentSection = $('.section-group .in'); if (currentSection.length > 0) { - initSection(`#${currentSection.attr('id')}`); + initSection(`${currentSection.attr('id')}`); } // Handle the new section - initSection('#new_section_new_section'); + // initSection('#new_section_section_description'); + Tinymce.init({ selector: '#new_section_section_description' }); }); diff --git a/app/javascript/src/orgAdmin/templates/edit.js b/app/javascript/src/orgAdmin/templates/edit.js index 6566fba161..c5cdf68794 100644 --- a/app/javascript/src/orgAdmin/templates/edit.js +++ b/app/javascript/src/orgAdmin/templates/edit.js @@ -1,4 +1,4 @@ -import { Tinymce } from '../../utils/tinymce.js.erb'; +import { Tinymce } from '../../utils/tinymce.js'; import { eachLinks } from '../../utils/links'; import { isObject, isString } from '../../utils/isType'; import { renderNotice, renderAlert } from '../../utils/notificationHelper'; @@ -6,7 +6,7 @@ import { scrollTo } from '../../utils/scrollTo'; $(() => { Tinymce.init({ - selector: '.template', + selector: '#template_description', init_instance_callback(editor) { // When the text editor changes to blank, set the corresponding destroy // field to true (if present). diff --git a/app/javascript/src/orgAdmin/templates/new.js b/app/javascript/src/orgAdmin/templates/new.js index 3e843df0bd..a0e20c6606 100644 --- a/app/javascript/src/orgAdmin/templates/new.js +++ b/app/javascript/src/orgAdmin/templates/new.js @@ -1,9 +1,9 @@ -import { Tinymce } from '../../utils/tinymce.js.erb'; +import { Tinymce } from '../../utils/tinymce.js'; import { eachLinks } from '../../utils/links'; $(() => { Tinymce.init({ - selector: '.template', + selector: '#template_description', init_instance_callback(editor) { // When the text editor changes to blank, set the corresponding destroy // field to true (if present). diff --git a/app/javascript/src/orgs/adminEdit.js b/app/javascript/src/orgs/adminEdit.js index cd10a2fdb8..9a1abc3c73 100644 --- a/app/javascript/src/orgs/adminEdit.js +++ b/app/javascript/src/orgs/adminEdit.js @@ -1,7 +1,7 @@ // TODO: we need to be able to swap in the appropriate locale here import 'number-to-text/converters/en-us'; import { isObject } from '../utils/isType'; -import { Tinymce } from '../utils/tinymce.js.erb'; +import { Tinymce } from '../utils/tinymce.js'; import { eachLinks } from '../utils/links'; import { initAutocomplete, scrubOrgSelectionParamsOnSubmit } from '../utils/autoComplete'; diff --git a/app/javascript/src/plans/editDetails.js b/app/javascript/src/plans/editDetails.js index f32b817240..17db584b7e 100644 --- a/app/javascript/src/plans/editDetails.js +++ b/app/javascript/src/plans/editDetails.js @@ -1,5 +1,5 @@ import { initAutocomplete, scrubOrgSelectionParamsOnSubmit } from '../utils/autoComplete'; -import { Tinymce } from '../utils/tinymce.js.erb'; +import { Tinymce } from '../utils/tinymce.js'; import toggleConditionalFields from '../utils/conditionalFields'; import getConstant from '../utils/constants'; @@ -10,8 +10,8 @@ $(() => { const form = $('form.edit_plan'); if (form.length > 0) { - Tinymce.init({ selector: '#plan_description' }); - Tinymce.init({ selector: '#plan_ethical_issues_description' }); + Tinymce.init({ selector: 'textarea#plan_description' }); + Tinymce.init({ selector: 'textarea#plan_ethical_issues_description' }); $('#is_test').click((e) => { $('#plan_visibility').val($(e.target).is(':checked') ? 'is_test' : 'privately_visible'); diff --git a/app/javascript/src/researchOutputs/form.js b/app/javascript/src/researchOutputs/form.js index b454b9eb3d..c3df1eb317 100644 --- a/app/javascript/src/researchOutputs/form.js +++ b/app/javascript/src/researchOutputs/form.js @@ -1,6 +1,6 @@ import getConstant from '../utils/constants'; import { isUndefined, isObject } from '../utils/isType'; -import { Tinymce } from '../utils/tinymce.js.erb'; +import { Tinymce } from '../utils/tinymce.js'; $(() => { const form = $('.research_output_form'); diff --git a/app/javascript/src/superAdmin/notifications/edit.js b/app/javascript/src/superAdmin/notifications/edit.js index 03cac9a60b..3587eee0d7 100644 --- a/app/javascript/src/superAdmin/notifications/edit.js +++ b/app/javascript/src/superAdmin/notifications/edit.js @@ -1,11 +1,11 @@ -import { Tinymce } from '../../utils/tinymce.js.erb'; +import { Tinymce } from '../../utils/tinymce.js'; // add the info on selecting the check from notification suitable import { paginableSelector } from '../../utils/paginable'; import * as notifier from '../../utils/notificationHelper'; $(() => { - Tinymce.init({ selector: '.notification-text', forced_root_block: '' }); + Tinymce.init({ selector: '#notification_body', forced_root_block: '' }); $(paginableSelector).on('click, change', '.enable_notification input[type="checkbox"]', (e) => { const form = $(e.target).closest('form'); diff --git a/app/javascript/src/superAdmin/themes/newEdit.js b/app/javascript/src/superAdmin/themes/newEdit.js index d838e77006..81b51bf55f 100644 --- a/app/javascript/src/superAdmin/themes/newEdit.js +++ b/app/javascript/src/superAdmin/themes/newEdit.js @@ -1,4 +1,4 @@ -import { Tinymce } from '../../utils/tinymce.js.erb'; +import { Tinymce } from '../../utils/tinymce.js'; $(() => { Tinymce.init({ selector: '#theme_description' }); diff --git a/app/javascript/src/utils/conditionalFields.js b/app/javascript/src/utils/conditionalFields.js index df4a2af42f..d545a1a099 100644 --- a/app/javascript/src/utils/conditionalFields.js +++ b/app/javascript/src/utils/conditionalFields.js @@ -6,7 +6,7 @@ // For example see: app/views/plans/_edit_details.html.erb // app/javascript/src/plans/editDetails.js // -import { Tinymce } from './tinymce.js.erb'; +import { Tinymce } from './tinymce.js'; // Expecting `context` to be the field that triggers the hide/show of the corresponding fields export default function toggleConditionalFields(context, showThem) { diff --git a/app/javascript/src/utils/tinymce.js.erb b/app/javascript/src/utils/tinymce.js similarity index 61% rename from app/javascript/src/utils/tinymce.js.erb rename to app/javascript/src/utils/tinymce.js index 0d0ad822b1..3c7eb73c47 100644 --- a/app/javascript/src/utils/tinymce.js.erb +++ b/app/javascript/src/utils/tinymce.js @@ -1,21 +1,24 @@ // Import TinyMCE import tinymce from 'tinymce/tinymce'; -// Import TinyMCE theme -import 'tinymce/themes/silver/theme'; + +// TinyMCE DOM helpers +import 'tinymce/models/dom/'; + +// TinyMCE toolbar icons import 'tinymce/icons/default'; -// Plugins + +// TinyMCE theme +import 'tinymce/themes/silver'; + +// TinyMCE Plugins import 'tinymce/plugins/table'; import 'tinymce/plugins/lists'; import 'tinymce/plugins/autoresize'; import 'tinymce/plugins/link'; -import 'tinymce/plugins/paste'; import 'tinymce/plugins/advlist'; // Other dependencies -import { isObject, isString } from './isType'; - -// Pull in the rails helper functions -<% helpers = ActionController::Base.helpers %> +import { isObject, isString, isUndefined } from './isType'; // // Configuration extracted from // // https://www.tinymce.com/docs/advanced/usage-with-module-loaders/ @@ -24,7 +27,7 @@ export const defaultOptions = { statusbar: true, menubar: false, toolbar: 'bold italic | bullist numlist | link | table', - plugins: 'table autoresize link paste advlist lists', + plugins: 'table autoresize link advlist lists', browser_spellcheck: true, advlist_bullet_styles: 'circle,disc,square', // Only disc bullets display on htmltoword target_list: false, @@ -34,12 +37,14 @@ export const defaultOptions = { autoresize_bottom_margin: 10, branding: false, extended_valid_elements: 'iframe[tooltip] , a[href|target=_blank]', - paste_auto_cleanup_on_paste: true, - paste_remove_styles: true, - paste_convert_middot_lists: true, + paste_as_text: true, + paste_block_drop: true, + paste_merge_formats: true, + paste_tab_spaces: 4, + smart_paste: true, + paste_data_images: true, paste_remove_styles_if_webkit: true, - paste_remove_spans: true, - paste_strip_class_attributes: 'all', + paste_webkit_styles: 'none', table_default_attributes: { border: 1, }, @@ -48,17 +53,16 @@ export const defaultOptions = { skin_url: '/tinymce/skins/oxide', content_css: ['/tinymce/tinymce.css'], }; + /* - This function is invoked anytime a new editor is initialised (e.g. Tinymce.init()) - and shrinks a tinymce editor to the minimum height specified at autoresize_min_height - editor's settings. Since there are cases that tinymce editor is loaded in the DOM - but has display:none style, the iframe associated gets the height of the screen's device - and using this function there is no need to wait until the tinymce gains focus to be autoresized. -*/ -const resizeEditors = (editors) => { - editors.forEach((editor) => { - $(editor.iframeElement).height(editor.settings.autoresize_min_height); - }); + This function determines whether or not the editor is a TinyMCE editor + */ +const isTinymceEditor = (editor) => { + if (isObject(editor)) { + return editor.hasOwnProperty('id') && typeof editor.getContainer === 'function'; + } else { + return false; + } }; /* @@ -67,16 +71,14 @@ const resizeEditors = (editors) => { behind the scenes) to the Tinymce iframe so that screen readers read the correct label when the tinymce iframe receives focus. */ -const attachLabelToIframe = (tinymceContext, hiddenFieldSelector) => { - const iframe = $(tinymceContext).siblings('.mce-container').find('iframe'); - const hiddenField = $(hiddenFieldSelector); +const attachLabelToIframe = (editor) => { + if (isTinymceEditor(editor)) { + const iframe = editor.getContainer().querySelector('iframe'); + const lbl = document.querySelector(`label[for="${editor.id}"]`); - if (isObject(iframe) && isObject(hiddenField)) { - const id = hiddenField.attr('id'); - const lbl = iframe.closest('form').find(`label[for="${id}"]`); - if (isObject(lbl)) { - // Connect the label to the iframe - lbl.attr('for', iframe.attr('id')); + // If the iframe and label could be found, then set the label's 'for' attribute to the id of the iframe + if (isObject(iframe) && isObject(lbl)) { + lbl.setAttribute('for', iframe.getAttribute('id')); } } }; @@ -88,17 +90,23 @@ export const Tinymce = { @param options - An object with tinyMCE properties */ init(options = {}) { - if (isObject(options)) { - tinymce.init($.extend(true, defaultOptions, options)).then(resizeEditors); - } else { - tinymce.init(defaultOptions).then(resizeEditors); - } + // If any options were specified, merge them with the default options. + let opts = { + ...defaultOptions, + ...options + }; - // Connect the label to the Tinymce iframe - $(options.selector).each((idx, el) => { - attachLabelToIframe(el, options.selector); + tinymce.init(opts).then((editors) => { + if (editors.length > 0) { + for (const editor of editors) { + // auto-resize the editor and connect the form label to the TinyMCE iframe + editor.execCommand('mceAutoResize'); + attachLabelToIframe(editor, editor.id); + } + } }); }, + /* Finds any tinyMCE editor whose target element/textarea has the className passed @param className - A string representing the class name of the tinyMCE editor @@ -107,12 +115,11 @@ export const Tinymce = { */ findEditorsByClassName(className) { if (isString(className)) { - return tinymce.editors.reduce((acc, e) => { - if ($(e.getElement()).hasClass(className)) { - return acc.concat([e]); - } - return acc; - }, []); + const elements = Array.from(document.getElementsByClassName(className)); + // Fetch the textarea elements and then return the TinyMCE editors associated with the element ids + return elements.map((el) => { + return Tinymce.findEditorById(el.getAttribute('id')); + }); } return []; }, @@ -124,7 +131,7 @@ export const Tinymce = { */ findEditorById(id) { if (isString(id)) { - return tinymce.editors.find(el => el.id === id); + return tinymce.get(id); } return undefined; }, @@ -137,7 +144,14 @@ export const Tinymce = { */ destroyEditorsByClassName(className) { const editors = this.findEditorsByClassName(className); - editors.forEach(ed => ed.destroy(false)); + if (editors.length > 0) { + /* editors.forEach(ed => ed.destroy(false)); */ + for (const editor of editors) { + if (isTinymceEditor(editor)) { + editor.destroy(false); + } + } + } }, /* Destroy an editor instance whose target element/textarea has HTML id passed. This method @@ -146,7 +160,7 @@ export const Tinymce = { */ destroyEditorById(id) { const editor = this.findEditorById(id); - if (editor) { + if (isTinymceEditor(editor)) { editor.destroy(false); } }, diff --git a/app/views/org_admin/conditions/_webhook_form.html.erb b/app/views/org_admin/conditions/_webhook_form.html.erb index 6613139bb6..8230e61b63 100644 --- a/app/views/org_admin/conditions/_webhook_form.html.erb +++ b/app/views/org_admin/conditions/_webhook_form.html.erb @@ -9,33 +9,33 @@