Bugzilla – Attachment 174868 Details for
Bug 11583
Add WYSIWYG editor for HTML notices/letters
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 11583: Add WYSIWYG editor to HTML notices/letters
Bug-11583-Add-WYSIWYG-editor-to-HTML-noticesletter.patch (text/plain), 14.12 KB, created by
David Cook
on 2024-11-21 06:04:34 UTC
(
hide
)
Description:
Bug 11583: Add WYSIWYG editor to HTML notices/letters
Filename:
MIME Type:
Creator:
David Cook
Created:
2024-11-21 06:04:34 UTC
Size:
14.12 KB
patch
obsolete
>From 5c45eef86e634e3ca4524b842344dd59de3ab994 Mon Sep 17 00:00:00 2001 >From: David Cook <dcook@prosentient.com.au> >Date: Wed, 23 Oct 2024 05:45:03 +0000 >Subject: [PATCH] Bug 11583: Add WYSIWYG editor to HTML notices/letters > >This change adds a WYSIWYG editor to HTML notices/letters. > >Test plan: >0. Apply the patch >1. In "Local use" system preferences, create "UseWYSIWYGinNotices" >and set it to "1" >2. Go to http://localhost:8081/cgi-bin/koha/tools/letter.pl >3. Edit any template >4. Open the "Email" tab >5. Toggle "HTML message" on/off and see the WYSIWYG editor appear >and disappear >6. Try adding content with the "Insert->" button >7. For notices like AUTO_RENEWALS_DGST, try the "View default" >and "Copy to template" button. >(NOTE: You'll want to wrap the AUTO_RENEWALS_DGST text with <pre></pre> >so that it doesn't destroy your whitespace when converting to HTML) > >8. Try adding text and formatting using the WYSIWYG >9. Try adding text and formatting using the "<>" source code button > >NOTE: If you click on a template token in the WYSIWYG, you should >notice that it's uneditable. You must edit it in source code mode. >This is because in the WYSIWYG the template token has been converted >to a HTML friendly placeholder. >--- > .../prog/en/modules/tools/letter.tt | 13 +- > .../intranet-tmpl/prog/js/letter_editor.js | 208 ++++++++++++++++++ > 2 files changed, 217 insertions(+), 4 deletions(-) > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/letter_editor.js > >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/letter.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/letter.tt >index 784d15d0f8..d21da2881e 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/letter.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/letter.tt >@@ -648,15 +648,15 @@ > <label for="is_html_[% letter.message_transport_type | html %]_[% lang | html %]">HTML message:</label> > [% IF letter.is_html %] > [% IF sms_readonly %] >- <input type="checkbox" name="is_html_[% letter.message_transport_type | html %]_[% lang | html %]" id="is_html_[% letter.message_transport_type | html %]_[% lang | html %]" value="1" checked="checked" disabled="disabled" /> >+ <input type="checkbox" name="is_html_[% letter.message_transport_type | html %]_[% lang | html %]" class="is_html_chkbox" id="is_html_[% letter.message_transport_type | html %]_[% lang | html %]" value="1" checked="checked" disabled="disabled" /> > [% ELSE %] >- <input type="checkbox" name="is_html_[% letter.message_transport_type | html %]_[% lang | html %]" id="is_html_[% letter.message_transport_type | html %]_[% lang | html %]" value="1" checked="checked" /> >+ <input type="checkbox" name="is_html_[% letter.message_transport_type | html %]_[% lang | html %]" class="is_html_chkbox" id="is_html_[% letter.message_transport_type | html %]_[% lang | html %]" value="1" checked="checked" /> > [% END %] > [% ELSE %] > [% IF sms_readonly %] >- <input type="checkbox" name="is_html_[% letter.message_transport_type | html %]_[% lang | html %]" id="is_html_[% letter.message_transport_type | html %]_[% lang | html %]" value="1" disabled="disabled" /> >+ <input type="checkbox" name="is_html_[% letter.message_transport_type | html %]_[% lang | html %]" class="is_html_chkbox" id="is_html_[% letter.message_transport_type | html %]_[% lang | html %]" value="1" disabled="disabled" /> > [% ELSE %] >- <input type="checkbox" name="is_html_[% letter.message_transport_type | html %]_[% lang | html %]" id="is_html_[% letter.message_transport_type | html %]_[% lang | html %]" value="1" /> >+ <input type="checkbox" name="is_html_[% letter.message_transport_type | html %]_[% lang | html %]" class="is_html_chkbox" id="is_html_[% letter.message_transport_type | html %]_[% lang | html %]" value="1" /> > [% END %] > [% END %] > </li> >@@ -835,6 +835,11 @@ > }); > </script> > [% Asset.js("js/letter.js") | $raw %] >+ [% IF Koha.Preference('UseWYSIWYGinNotices') %] >+ [% Asset.js("lib/tiny_mce/tinymce.min.js") | $raw %] >+ [% INCLUDE 'str/tinymce_i18n.inc' %] >+ [% Asset.js("js/letter_editor.js") | $raw %] >+ [% END %] > [% END %] > > [% INCLUDE 'intranet-bottom.inc' %] >diff --git a/koha-tmpl/intranet-tmpl/prog/js/letter_editor.js b/koha-tmpl/intranet-tmpl/prog/js/letter_editor.js >new file mode 100644 >index 0000000000..232a63275b >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/letter_editor.js >@@ -0,0 +1,208 @@ >+ function initTinyMce(textarea){ >+ let editor = tinyMCE.init({ >+ branding : false, >+ block_unsupported_drop : false, >+ custom_elements:"style,link,~link", >+ content_style: ` >+ pre { >+ all: unset; >+ display: block; >+ white-space: pre; >+ } >+ `, >+ extended_valid_elements:"style,link[href|rel]", >+ forced_root_block : '', //this is applied between BeforeSetContent and SetContent >+ menubar : "file edit view insert format tools table", >+ plugins: "autoresize code", >+ //plugins : "autoresize table hr link image charmap lists code emoticons", >+ autoresize_bottom_margin: 60, >+ relative_urls : false, >+ selector: "#" + textarea.id, >+ verify_html: false, >+ toolbar: [ >+ "formatselect | bold italic | cut copy paste | alignleft aligncenter alignright | outdent indent | image link unlink anchor cleanup hr", >+ "table | bullist numlist | undo redo | removeformat | emoticons charmap | forecolor backcolor | code" >+ ], >+ entities: '', >+ entity_encoding: 'raw', >+ cleanup: false, >+ remove_linebreaks: false, >+ preserve_newlines: true, >+ setup: function(editor){ >+ editor.on('change', function () { >+ editor.save(); // This updates the textarea on each change >+ }); >+ editor.on('BeforeSetContent', function(e){ >+ //NOTE: this gets called by undo with "raw" format, >+ //and we don't want to double-replace template syntax tokens >+ if (e.format == 'html'){ >+ //NOTE: At this point, all we have is a string, so we tokenize and put in HTML-friendly placeholders >+ e.content = e.content.replace(/\[%([\s\S]*?)%\]/g, function(match, content) { >+ let encoded_content = btoa(content); >+ let placeholder = `<span class="tmpl-tag template" contenteditable="false" data-original="${encoded_content}">[%${content}%]</span>`; >+ return placeholder; >+ }); >+ e.content = e.content.replace(/<<([\s\S]*?)>>/g, function(match, content) { >+ let encoded_content = btoa(content); >+ let rv = `<span class="tmpl-tag legacy_template" contenteditable="false" data-original="${encoded_content}"><<${content}>></span>`; >+ return rv; >+ }); >+ } >+ }); >+ editor.on('GetContent', function(e){ >+ //NOTE: GetContent fires after the content is serialized from the editor. >+ //NOTE: We re-parse the content, and apply our own custom serializer, which >+ //converts HTML placeholders back into template tokens using the appropriate syntax. >+ //NOTE: We need a custom serializer, because otherwise our template tokens will be >+ //escaped by the HTML serializer as if they were HTML when they're not. >+ const parser = new DOMParser(); >+ const doc = parser.parseFromString(e.content, "text/html"); >+ if (doc){ >+ const text = serialize_template(doc); >+ if (text){ >+ e.content = text; >+ } >+ } >+ //NOTE: If we don't like the DOM-based method, there is a regex method, which works reasonably well: >+ /* >+ try { >+ e.content = e.content.replace(/<span class="tmpl-tag template" contenteditable="false" data-original="([\s\S]*?)">\[%([\s\S]*?)%\]<\/span>/g,function (match,content){ >+ let decoded_content = atob(content); >+ let rv = `[%${decoded_content}%]`; >+ return rv; >+ }); >+ } catch(error) { >+ console.error(error); >+ } >+ */ >+ /* >+ try { >+ e.content = e.content.replace(/<span class="tmpl-tag legacy_template" contenteditable="false" data-original="([\s\S]*?)"><<([\s\S]*?)>><\/span>/g,function (match,content){ >+ let decoded_content = atob(content); >+ let rv = `<<${decoded_content}>>`; >+ return rv; >+ }); >+ } catch(error){ >+ console.error(error); >+ } >+ */ >+ }); >+ }, >+ }); >+ return editor; >+ } >+ >+ const void_elements = { >+ "area": true, >+ "base": true, >+ "br": true, >+ "col": true, >+ "embed": true, >+ "hr": true, >+ "img": true, >+ "input": true, >+ "link": true, >+ "meta": true, >+ "param": true, >+ "source": true, >+ "track": true, >+ "wbr": true >+ }; >+ >+ //NOTE: This function is a custom serializer >+ function processNode(node) { >+ let result = ''; >+ if (node.nodeType === Node.TEXT_NODE) { >+ // For text nodes, simply append the raw content >+ result += node.nodeValue; >+ } else if (node.nodeType === Node.ELEMENT_NODE) { >+ // For element nodes, append the tag and recursively traverse children >+ let node_name = node.tagName.toLowerCase(); >+ >+ let skip = 0; >+ if (node_name == 'body'){ >+ skip = 1 ; >+ } >+ >+ if ( node_name == 'span' && node.classList.contains('template') ){ >+ const original_text = node.dataset.original; >+ let decoded_content = atob(original_text); >+ result += `[%${decoded_content}%]`; >+ } >+ else if ( node_name == 'span' && node.classList.contains('legacy_template') ) { >+ const original_text = node.dataset.original; >+ let decoded_content = atob(original_text); >+ result += `<<${decoded_content}>>`; >+ } >+ else { >+ //NOTE: This is all other regular nodes >+ if ( !skip ) { >+ result += `<${node.tagName.toLowerCase()}`; >+ for ( let attr of node . attributes ) { >+ result += ` ${attr.name}="${attr.value}"`; >+ } >+ result += `>`; >+ } >+ >+ // Process child nodes >+ for (let child of node.childNodes) { >+ result += processNode(child); >+ } >+ if (!skip){ >+ if (! void_elements[node_name]){ >+ result += `</${node.tagName.toLowerCase()}>`; >+ } >+ } >+ } >+ } >+ return result; >+ } >+ >+ function serialize_template(doc) { >+ return processNode(doc.body); >+ } >+ >+ let email_inputs = document.querySelectorAll('.content_email'); >+ email_inputs.forEach((email_input) => { >+ let containerid = email_input.id.replace(/^content_/,""); >+ let panel = $("#" + containerid + "_panel"); >+ let html_btn = document.querySelector(`#is_html_${containerid}`); >+ if (html_btn.checked){ >+ initTinyMce(email_input); >+ } >+ $(`button[data-containerid=${containerid}`).on('click',function(){ >+ let myListBox = $(panel).find('select[name="SQLfieldname"]'); >+ if($(myListBox).find('option').length > 0) { >+ $(myListBox).find('option').each( function (){ >+ if ( $(this).prop('selected') && $(this).val().length > 0 ) { >+ let editor = tinyMCE.get(email_input.id); >+ if (editor){ >+ editor.insertContent("<<" + $(this).val() + ">>"); >+ } >+ } >+ }); >+ } >+ }); >+ }); >+ $("#noticeSampleModal").on("click", ".copy", function(){ >+ let content = $('#noticeSampleModal .template-body').text(); >+ let replaceid = $('#noticeSampleModal').data('replaceid'); >+ let editor = tinyMCE.get(replaceid); >+ if (editor){ >+ editor.setContent(content); >+ } >+ }); >+ $(".is_html_chkbox").on("click", function(el){ >+ const target = el.target; >+ let shortened_id = target.id.replace(/^is_html_/,""); >+ let textarea_id = `content_${shortened_id}`; >+ if (target.checked){ >+ let textarea = document.querySelector(`#${textarea_id}`); >+ initTinyMce(textarea); >+ } else { >+ let editor = tinyMCE.get(textarea_id); >+ if (editor){ >+ editor.remove(); >+ } >+ } >+ }); >-- >2.39.5
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 11583
: 174868