1// https://civicrm.org/licensing 2(function($, _) { 3 // This defines an interface which by default only handles plain textareas 4 // A wysiwyg implementation can extend this by overriding as many of these functions as needed 5 6 let richTextInputId = 0; 7 8 /** 9 * A rich text input with a preview mode 10 * <civi-rich-text-input></civi-rich-text-input> 11 * 12 */ 13 class CiviRichTextInput extends HTMLElement { 14 15 constructor() { 16 super(); 17 18 // initialise child input 19 // NOTE: we need to do this here rather than render in order to persist 20 // the input value across connection/disconnection 21 this.input = document.createElement('textarea'); 22 // generate a unique id for the element as required by ckeditor etc 23 this.input.id = 'civiRichTextInput' + richTextInputId++; 24 this.input.style.display = 'none'; 25 } 26 27 connectedCallback() { 28 this.render(); 29 } 30 31 render() { 32 this.innerHTML = ` 33 <div class="rich-text-toolbar crm-buttons" style="display: none;"> 34 <button type="button" class="crm-button rich-text-cancel"> 35 <i class="crm-i fa-cancel" role="img" aria-disabled="true"></i> 36 </button> 37 <button type="button" class="crm-button rich-text-save"> 38 <i class="crm-i fa-check" role="img" aria-disabled="true"></i> 39 </button> 40 </div> 41 <div class="rich-text-preview" tabindex="0"></div> 42 `; 43 44 // reappend the child input 45 this.append(this.input); 46 47 this.toolbar = this.querySelector('.rich-text-toolbar'); 48 this.cancelButton = this.querySelector('.rich-text-cancel'); 49 this.saveButton = this.querySelector('.rich-text-save'); 50 this.preview = this.querySelector('.rich-text-preview'); 51 52 // add translated text 53 this.cancelButton.append(ts('Cancel')); 54 this.saveButton.append(ts('Save')); 55 this.preview.title = ts('Click to edit'); 56 57 // (re)load any content 58 this.renderPreview(); 59 60 // open the editor when click/type on preview 61 this.preview.onclick = (e) => { 62 e.preventDefault(); 63 this.openEditor(); 64 }; 65 this.preview.onkeystroke = (e) => { 66 e.preventDefault(); 67 this.openEditor(); 68 }; 69 // close the editor with the buttons 70 this.cancelButton.onclick = () => this.closeEditor(); 71 this.saveButton.onclick = () => this.saveAndCloseEditor(); 72 73 // load token picker if requested 74 if (this.hasAttribute('token-picker')) { 75 this.loadTokenPicker(); 76 } 77 } 78 79 renderPreview() { 80 // if no content set, show an edit icon instead 81 this.preview.innerHTML = this.value.length ? this.value : '<i class="crm-i fa-pencil" role="img" aria-label="Edit"></i>'; 82 } 83 84 loadTokenPicker() { 85 this.tokenPicker = document.createElement('input'); 86 this.tokenPicker.classList.add('rich-text-token-picker', 'form-control', 'crm-auto-width', 'crm-action-menu', 'fa-code', 'collapsible-optgroups'); 87 88 this.toolbar.prepend(this.tokenPicker); 89 90 this.tokenPicker.onchange = () => { 91 const token = `[${this.tokenPicker.value}]`; 92 CRM.wysiwyg.insert(this.input, token); 93 CRM.$(this.tokenPicker).select2('val', ''); 94 }; 95 96 CRM.$(this.tokenPicker).crmSelect2({ 97 data: () => this.getTokens(), 98 placeholder: ts('Tokens') 99 }); 100 } 101 102 openEditor() { 103 this.setAttribute('editing', true); 104 CRM.wysiwyg.create(this.input); 105 this.preview.style.display = 'none'; 106 this.toolbar.style.display = null; 107 this.dispatchEvent(new Event('load')); 108 } 109 110 closeEditor() { 111 CRM.wysiwyg.destroy(this.input); 112 this.input.style.display = 'none'; 113 this.toolbar.style.display = 'none'; 114 this.preview.style.display = null; 115 this.removeAttribute('editing'); 116 } 117 118 saveAndCloseEditor() { 119 this.value = CRM.wysiwyg.getVal(this.input); 120 this.closeEditor(); 121 this.dispatchEvent(new Event('change')); 122 } 123 124 getTokens() { 125 if (this.closest('af-gui-editor')) { 126 const afGuiEditor = angular.element(this.closest('af-gui-editor')).controller('afGuiEditor'); 127 return { 128 results: afGuiEditor.getTokens(this.hasAttribute('include-submission-tokens')) 129 }; 130 } 131 else { 132 throw new Error('civi-rich-text-input[token-picker] doesn\'t know how to get available tokens outside of af-gui-editor context yet'); 133 } 134 } 135 136 get value() { 137 return this.input.value; 138 } 139 140 set value(v) { 141 if (!v) { 142 v = ''; 143 } 144 this.input.value = v;
145 CRM.wysiwyg.setVal(this.input, v); 146 if (this.preview) { 147 this.renderPreview(); 148 } 149 } 150 } 151 152 customElements.define('civi-rich-text-input', CiviRichTextInput); 153 154 155 CRM.wysiwyg = { 156 supportsFileUploads: !!CRM.config.wysisygScriptLocation, 157 create: function(item) { 158 var ret = $.Deferred(); 159 // Lazy-load the wysiwyg js 160 if (CRM.config.wysisygScriptLocation) { 161 CRM.loadScript(CRM.config.wysisygScriptLocation).done(function() { 162 CRM.wysiwyg._create(item).done(function() { 163 ret.resolve(); 164 }); 165 }); 166 } else { 167 ret.resolve(); 168 } 169 return ret; 170 }, 171 destroy: _.noop, 172 updateElement: _.noop, 173 getVal: function(item) { 174 return $(item).val(); 175 }, 176 setVal: function(item, val) { 177 return $(item).val(val); 178 }, 179 insert: function(item, text) { 180 CRM.wysiwyg._insertIntoTextarea(item, text); 181 }, 182 focus: function(item) { 183 $(item).focus(); 184 }, 185 // Fallback function to use when a wysiwyg has not been initialized 186 _insertIntoTextarea: function(item, text) { 187 var itemObj = $(item); 188 var origVal = itemObj.val(); 189 var origStart = itemObj[0].selectionStart; 190 var origEnd = itemObj[0].selectionEnd; 191 var newVal = origVal.substring(0, origStart) + text + origVal.substring(origEnd); 192 itemObj.val(newVal); 193 var newPos = (origStart + text.length); 194 itemObj[0].selectionStart = newPos; 195 itemObj[0].selectionEnd = newPos; 196 itemObj.triggerHandler('change'); 197 CRM.wysiwyg.focus(item); 198 }, 199 // Create a "collapsed" textarea that expands into a wysiwyg when clicked 200 createCollapsed: (item) => { 201 const customInput = document.createElement('civi-rich-text-input'); 202 // use the pre-existing textarea (preserve name etc) for the custom element 203 customInput.input = item; 204 customInput.input.style.display = 'none'; 205 // replace the pre-existing textarea with our custom element 206 item.replaceWith(customInput); 207 } 208 }; 209 210})(CRM.$, CRM._);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.