PageSourceSearch

https://alp.org/modules/civicrm/js/wysiwyg/crm.wysiwyg.js?tls0vx

js alp.org collected 2026-09-24 18:15:45 UTC 6,676 bytes, 210 lines download raw bytes

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.