PageSourceSearch

https://recruitment.pupillagegateway.com/members/tpl/x/richtextedi…=3d3a95127ff547363bebdb6cc007bd7d5a654a60

js pupillagegateway.com collected 2026-10-02 16:32:05 UTC 30,497 bytes, 907 lines download raw bytes

1// NOTE: we can move editor specific things to their own place if we have more editors.
2var froalaKey = froalaKey || 'Ja2A4wA1D2D2B1H5B3I3oCd2ZSb1XHi1Cb2a1KIWCWMJHXCLSwG1G1B2C3B1D7E6E1F4B3==';
3
4
5// video plugin patch to intercept `insertByUrl`
6// and fetch the actual embed code rather than froala building up one using just url
7// froala's video plugin only takes the url and just inserts that url into
8// it's own iframe template which has no way of getting the title.
9FroalaEditor.PLUGINS.safelyPatchedVideoPlugin = function (editor) {
10  const originalInsert = editor.video.insert;
11  const fallbackInsertByURL = editor.video.insertByURL;
12
13  // Patch insertByURL
14  editor.video.insertByURL = function () {
15    const popup = editor.popups.get('video.insert');
16    const input = popup.find('.fr-video-by-url-layer input[type="text"]');
17    const url = (input.val() || '').trim();
18
19    if (!/^http/.test(url)) {
20      return fallbackInsertByURL.call(this); // fallback to original
21    }
22
23    // Use YouTube oEmbed for accurate iframe with title
24    if (url.includes('youtube.com') || url.includes('youtu.be')) {
25      fetchYouTubeEmbedHTML(url)
26          .then(function (iframeHTML) {
27            if (iframeHTML) {
28              originalInsert.call(editor.video, iframeHTML);
29            } else {
30              editor.events.trigger('video.linkError', [url]);
31            }
32          })
33          .catch(function () {
34            fallbackInsertByURL.call(editor.video);
35          });
36    } else {
37      // Not YouTube? Use original Froala logic
38      fallbackInsertByURL.call(this);
39    }
40  };
41
42  // fetch embed info from youtube
43  function fetchYouTubeEmbedHTML(videoUrl) {
44    return new Promise(function (resolve, reject) {
45      const oembedURL = 'https://www.youtube.com/oembed?url=' + encodeURIComponent(videoUrl) + '&format=json';
46      const xhr = new XMLHttpRequest();
47      xhr.open('GET', oembedURL, true);
48      xhr.onreadystatechange = function () {
49        if (xhr.readyState === 4) {
50          if (xhr.status === 200) {
51            try {
52              const json = JSON.parse(xhr.responseText);
53              resolve(json.html);
54            } catch (e) {
55              reject(e);
56            }
57          } else {
58            reject(xhr.status);
59          }
60        }
61      };
62      xhr.onerror = reject;
63      xhr.send();
64    });
65  }
66
67  return {};
68};
69
70
71
72/**
73 * The collection of available rich text editors.
74 *
75 * @enum {string}
76 */
77var RTEditor = RTEditor || {
78  FROALA: 'FROALA'
79}
80
81/**
82 * The collection of available content export formats.
83 *
84 * @enum {string}
85 */
86var RTEContentFormat = RTEContentFormat || {
87  HTML: 'html',
88  TEXT: 'text',
89}
90
91var htmlAllowedTags = htmlAllowedTags || ['a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo', 'blockquote', 'br', 'button', 'canvas', 'caption', 'cite', 'code', 'col', 'colgroup', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link', 'main', 'map', 'mark', 'menu', 'menuitem', 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'pre', 'progress', 'queue', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'style', 'section', 'select', 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'u', 'ul', 'var', 'video', 'wbr'];
92var htmlAllowedAttrs = htmlAllowedAttrs || ['accept', 'accept-charset', 'accesskey', 'action', 'align', 'allowfullscreen', 'allowtransparency', 'alt', 'aria-.*', 'async', 'autocomplete', 'autofocus', 'autoplay', 'autosave', 'background', 'bgcolor', 'border', 'charset', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'color', 'cols', 'colspan', 'content', 'contenteditable', 'contextmenu', 'controls', 'coords', 'data', 'data-.*', 'datetime', 'default', 'defer', 'dir', 'dirname', 'disabled', 'download', 'draggable', 'dropzone', 'enctype', 'for', 'form', 'formaction', 'frameborder', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'http-equiv', 'icon', 'id', 'ismap', 'itemprop', 'keytype', 'kind', 'label', 'lang', 'language', 'list', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'mozallowfullscreen', 'multiple', 'muted', 'name', 'novalidate', 'open', 'optimum', 'pattern', 'ping', 'placeholder', 'playsinline', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'reversed', 'rows', 'rowspan', 'sandbox', 'scope', 'scoped', 'scrolling', 'seamless', 'selected', 'shape', 'size', 'sizes', 'span', 'src', 'srcdoc', 'srclang', 'srcset', 'start', 'step', 'summary', 'spellcheck', 'style', 'tabindex', 'target', 'title', 'type', 'translate', 'usemap', 'value', 'valign', 'webkitallowfullscreen', 'width', 'wrap'];
93
94// collect csrf token
95var metaTagElement = document.querySelector("meta[name='csrf-token']");
96var rteUploadParams = rteUploadParams || {
97  _token: metaTagElement ? metaTagElement.getAttribute('content') : '',
98};
99
100/**
101 * Rich Text Editor.
102 *
103 * @param {string} editorName - The name of the editor to instantiate.
104 * @param {Object} options - Configuration options for the editor.
105 * @throws {Error}
106 * @returns RichTexhEditor
107 */
108var RichTextEditor = RichTextEditor || function (editorName = RTEditor.FROALA, options = { replace: true, editor: {} }) {
109  _stripVendorStyle = false;
110
111  // instance identifier string (attached/to be attached) to the dom element
112  this.identifier = RichTextEditor.COMPONENT_PREFIX + Math.random().toString(36).substring(2, 100);
113
114  // editor instance
115  var _editor = null;
116
117  // dom cache
118  RichTextEditor.cache = RichTextEditor.cache || {};
119
120  // Array of instantiated rich text editors.
121  RichTextEditor.instances = RichTextEditor.instances || {};
122
123  // preliminary checks
124  if (options === null || typeof options !== 'object') {
125    throw new Error(RichTextEditor.LOG_PREFIX + 'Invalid options supplied.');
126  }
127  if (!Object.values(RTEditor).includes(editorName)) {
128    throw new Error(RichTextEditor.LOG_PREFIX + 'Invalid editor name.');
129  }
130
131  /**
132   * Checks weather the editor is initialized or not.
133  *
134   * @throws {Error} when {_editor} not initialized.
135   */
136  this.checkInitialized = function () {
137    if(!_editor) {
138      throw new Error(RichTextEditor.LOG_PREFIX + 'Editor must be initialized first.');
139    }
140
141    return true;
142  }
143
144  // obtain editor config
145  var _config = {};
146
147  /**
148   * Attaches the editor instance to a DOM element.
149  *
150  * @param {string} selector - The CSS selector of the element to attach the editor to.
151  * @returns {RichTextEditor} The current instance for chaining.
152  */
153  this.attachTo = function (selector) {
154    if(RichTextEditor.elementBySelector(selector).length == 0 || RichTextEditor.elementBySelector(selector).length > 1) {
155      throw new Error(RichTextEditor.LOG_PREFIX + 'RichTextEditor can only be attached to exactly one element per instance.');
156    }
157
158    var existingIdentifier = this.alreadyAttached(selector);
159    if(existingIdentifier && RichTextEditor.instances[existingIdentifier] && options.replace) {
160      RichTextEditor.instances[existingIdentifier].destroy();
161    }
162
163    // obtain the config
164    _config = builRTEdConfig(selector, editorName, options.editor || {});
165
166    // https://tickets-tribepad.atlassian.net/browse/TCI-26006
167    _config.events = Object.assign({}, _config.events, {
168      'focus': function () {
169        $(selector).parent().find('div.fr-box').addClass('richtext-border-focus')
170      },
171      'blur': function(){
172        $(selector).parent().find('div.fr-box').removeClass('richtext-border-focus')
173      },
174      'keydown': function (e) {
175        if (e.key === 'Escape') {
176          if(this.opts.iframe){ // for iframe version 
177            // Focus on the wrapper element to maintain tab order position
178            const wrapper = this.$box[0];
179            if (wrapper) {
180              // Make wrapper focusable without adding to natural tab order
181              wrapper.setAttribute('tabindex', '-1');
182              
183              // Mark that we're intentionally focusing the wrapper from ESC
184              wrapper.setAttribute('data-esc-focused', 'true');
185              wrapper.focus();
186              
187              // Handle Tab key on the wrapper to navigate properly
188              const tabHandler = function(tabEvent) {
189                if (tabEvent.key === 'Tab') {
190                  if (!tabEvent.shiftKey) {
191                    // Tab forward - find next focusable element after the wrapper in DOM order
192                    tabEvent.preventDefault();
193                    
194                    // Find all focusable elements (excluding the wrapper itself)
195                    const focusableSelector = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex]:not([tabindex="-1"]), [contenteditable]';
196                    const allFocusable = Array.from(document.querySelectorAll(focusableSelector))
197                      .filter(el => el.offsetParent !== null && !wrapper.contains(el)); // Filter out hidden elements and elements inside wrapper
198                    
199                    // Find the first focusable element that comes after the wrapper in DOM order
200                    let nextElement = null;
201                    for (let i = 0; i < allFocusable.length; i++) {
202                      // Compare positions: if focusable element comes after wrapper, use it
203                      const position = wrapper.compareDocumentPosition(allFocusable[i]);
204                      // DOCUMENT_POSITION_FOLLOWING (4) means the element comes after wrapper
205                      if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
206                        nextElement = allFocusable[i];
207                        break;
208                      }
209                    }
210                    
211                    if (nextElement) {
212                      nextElement.focus();
213                    }
214                  }
215                  
216                  // Clean up after tab
217                  wrapper.removeAttribute('data-esc-focused');
218                  wrapper.removeEventListener('keydown', tabHandler);
219                }
220              };
221              
222              // Handle focus event on wrapper - if it's focused without ESC, pass focus to editor
223              const focusHandler = function(focusEvent) {
224                // Only handle if we're NOT in ESC-focused mode
225                if (!wrapper.hasAttribute('data-esc-focused')) {
226                  // Remove listener first to avoid recursion
227                  wrapper.removeEventListener('focus', focusHandler);
228                  // Focus the editor
229                  if (this.opts.iframe) {
230                    this.$iframe[0].contentWindow.focus();
231                  }
232                  this.events.focus(true);
233                }
234              }.bind(this);
235              
236              wrapper.addEventListener('keydown', tabHandler);
237              wrapper.addEventListener('focus', focusHandler);
238            }
239            this.events.trigger('blur');
240          }else{ // for non iframe version
241            this.events.trigger('blur');
242            this.el.blur();
243          }
244        }
245      }
246    });
247    
248
249    // get going with the editor
250    if (editorName === RTEditor.FROALA) {
251      _editor = new FroalaEditor(selector, _config, function(e) {
252
253        // use the patched plugin to insert actual embed code in youtube videos
254        this.opts.pluginsEnabled.push('safelyPatchedVideoPlugin');
255
256        // Utility function to fix double-encoded URLs
257        var fixDoubleEncodedUrls = function(content) {
258          if (!content || !content.includes('%252F')) {
259            return content;
260          }
261          return content.replace(/%252F/g, '%2F')
262                       .replace(/%252B/g, '%2B')
263                       .replace(/%253A/g, '%3A')
264                       .replace(/%253F/g, '%3F')
265                       .replace(/%253D/g, '%3D')
266                       .replace(/%2526/g, '%26');
267        };
268
269        // Replace URL sanitiser with a version which supports email variables and prevents double-encoding.
270        var oldSanitizeUrl = this.helpers.sanitizeURL;
271        this.helpers.sanitizeURL = function(url) {
272          // Skip sanitisation for email variables
273          if(url.match(/\{\$[a-z0-9\_]+\}/gi)) {
274            return url;
275          }
276          
277          // Check if URL is already encoded by looking for encoded characters
278          if(url.match(/%[0-9A-Fa-f]{2}/)) {
279            return url;
280          }
281
282          return oldSanitizeUrl(url);
283        }
284
285        // Fix double encoding in HTML output
286        var originalHtmlGet = this.html.get;
287        this.html.get = function(keepMarkers, keepClasses) {
288          var html = originalHtmlGet.call(this, keepMarkers, keepClasses);
289          return fixDoubleEncodedUrls(html);
290        };
291
292        // Fix double encoding directly in the editor content
293        var editor = this;
294        var fixEditorContent = function() {
295          if(editor.el && editor.el.innerHTML) {
296            var content = editor.el.innerHTML;
297            var fixedContent = fixDoubleEncodedUrls(content);
298            
299            // Only update if content actually changed
300            if (content !== fixedContent) {
301              // Save the current selection/cursor position
302              editor.selection.save();
303              
304              // Update the content
305              editor.el.innerHTML = fixedContent;
306              
307              // Restore the cursor position
308              editor.selection.restore();
309            }
310          }
311        };
312
313        // Trigger fix after content changes
314        // Commenting as it is causing TCI-28606
315        // this.events.on('contentChanged', function() {
316        //   setTimeout(fixEditorContent, 100);
317        // });
318
319        if (_config.isReadOnly) {
320          this.edit.off();
321        }
322
323        // Apply aria for accessibility
324        const ariaLabel = window.translations.froalaAriaLabel || 'Rich text editor. Press Escape to exit. Alt (Windows) or Option (Mac) + F10 for toolbar; Alt (Windows) or Option (Mac) + 0 for help.';
325        this.el.setAttribute('aria-label', ariaLabel);
326        this.el.setAttribute('aria-multiline', 'true');
327        this.el.setAttribute('role', 'textbox');
328      });
329    }
330
331    RichTextEditor.instances[this.identifier] = this;
332    RichTextEditor.elementBySelector(selector).attr('data-rte-identifier', this.identifier);
333
334    // _editor
335    if(_editor && _config.iframe) {
336      setTimeout(() => {
337        _editor.doc.body.classList.add('document-editor');
338      }, 0);
339
340      if(_stripVendorStyle) {
341        setTimeout(() => {
342          _editor.doc.querySelectorAll('head style[data-fr-style="true"]').forEach(el => el.remove());
343        }, 0);
344      }
345    }
346
347    return this;
348  }
349
350  /**
351   * Retrieves the editor instance.
352   *
353   * @returns {Object} The editor instance.
354   */
355  this.getEditor = function () {
356    this.checkInitialized();
357    return _editor;
358  }
359
360  /**
361   * Destroy properly.
362   *
363   */
364  this.destroy = function () {
365    try {
366      this.checkInitialized();
367      if(RichTextEditor.instances[this.identifier]) {
368        delete RichTextEditor.instances[this.identifier];
369      }
370      if(_editor.$oel && _editor.$oel.length === 1) {
371        _editor.$oel[0].removeAttribute('data-rte-identifier');
372      }
373      _editor.destroy();
374    } catch(err) {
375      // ignore
376    }
377  }
378
379  /**
380   * Determine if the selector is already attached
381   * to an RTE Instance
382   *
383   */
384  this.alreadyAttached = function (selector) {
385    return $(selector).data('rte-identifier') ?? false;
386  }
387
388  /**
389   * Retrieves the editor html contents as string.
390   *
391   * @returns {string} Contents of editor.
392   */
393  this.getContents = function (format = RTEContentFormat.HTML) {
394    this.checkInitialized();
395    var content = '';
396    if (editorName === RTEditor.FROALA) {
397      content = _editor.html.get();
398      switch (format) {
399        case RTEContentFormat.TEXT:
400          var tempElement = document.createElement('div');
401          tempElement.innerHTML = content;
402          content = tempElement.textContent || tempElement.innerText || '';
403          tempElement.remove();
404      }
405    }
406    return content;
407  }
408
409  return this;
410};
411
412// global prefix
413Object.defineProperty(RichTextEditor, 'COMPONENT_PREFIX', {
414  value: 'rte-', writable: false, configurable: false,
415});
416
417// log prefix
418Object.defineProperty(RichTextEditor, 'LOG_PREFIX', {
419  value: 'RichTextEditor: ', writable: false, configurable: false,
420});
421
422/**
423  * Return elements from cache first then dom.
424  *
425  * @param {string} selector - The CSS selector of the element to attach the editor to.
426  * @returns {object}.
427  */
428RichTextEditor.elementBySelector = RichTextEditor.elementBySelector || function(selector) {
429  return RichTextEditor.cache[selector] || (RichTextEditor.cache[selector] = $(selector));
430}
431
432RichTextEditor.destroyIfExists = function(item) {
433  if (!item) {
434    return;
435  }
436
437  for (const instance of Object.values(RichTextEditor.instances)) {
438    const editor = instance.getEditor?.();
439    const el = editor?.$oel?.[0];
440
441    if (el === item) {
442      instance.destroy();
443      break;
444    }
445  }
446};
447
448/**
449 * Constructs and returns the configuration object for the specified editor.
450 *
451 * @param {Object} selector - Selector of the HTML Element
452 * @param {string} editorName - The name of the editor to configure.
453 * @param {Object} options - Additional options to merge with the default configuration.
454 * @returns {Object} The complete configuration object for the specified editor.
455 */
456var builRTEdConfig = builRTEdConfig || function (selector, editorName, options) {
457  var config = {};
458
459  if (editorName === RTEditor.FROALA) {
460    // yeah there are cases,
461    if (!selector.classList && RichTextEditor.elementBySelector(selector).length === 1) {
462      selector = RichTextEditor.elementBySelector(selector)[0];
463    }
464
465    // Config for file/image/video uploads
466    let imageUploadConfig = {
467      imageUploadURL: '/v2/file/uploads/public/rte/image',
468      imageMaxSize: 25 * 1024 * 1024,
469      imageAllowedTypes: ['gif', 'jpeg', 'jpg', 'png'],
470      imageUploadParams: rteUploadParams,
471      imageMove: false,
472    };
473    let videoUploadConfig = {
474      videoUpload: true,
475      videoUploadURL: '/v2/file/uploads/public/rte/video',
476      videoMaxSize: 75 * 1024 * 1024,
477      videoAllowedTypes: ['avi', 'mp4', 'mov'],
478      videoUploadParams: rteUploadParams,
479    };
480
481    let fileUploadConfig = {
482      fileUploadURL: '/v2/file/uploads/public/rte/file',
483      fileMaxSize: 25 * 1024 * 1024,
484      fileUploadParams: rteUploadParams,
485    };
486
487    // All instances should have these
488    config.key = froalaKey;
489    config.heightMin = config.heightMin || 200;
490    config.useClasses = false;
491    config.enter = FroalaEditor.ENTER_P;
492    config.attribution = false;
493    config.tabSpaces = 4;
494    config.linkAlwaysBlank = false;
495
496    // Check for data-type attribute first
497    const dataType = selector.getAttribute('data-type');
498    if (dataType) {
499      switch (dataType) {
500        case 'job-description':
501          // Specific config for job descriptions
502          const editorConfig = {
503            attribution: false,
504            enter: FroalaEditor.ENTER_P,
505            events: {
506              'paste.before': function () {
507                const editor = this;
508                // Store original values
509                editor._originalAllowedTags = [...editor.opts.htmlAllowedTags];
510                editor._originalAllowedAttrs = [...editor.opts.htmlAllowedAttrs];
511
512                // Remove span and div from allowed tags
513                editor.opts.htmlAllowedTags = editor.opts.htmlAllowedTags.filter(tag => !['span', 'div'].includes(tag));
514                editor.opts.htmlAllowedAttrs = editor.opts.htmlAllowedAttrs.filter(attr => !['class', 'style'].includes(attr));
515              },
516              'paste.after': function () {
517                const editor = this;
518                // Restore original values
519                if (editor._originalAllowedTags) {
520                  editor.opts.htmlAllowedTags = editor._originalAllowedTags;
521                  delete editor._originalAllowedTags;
522                }
523                if (editor._originalAllowedAttrs) {
524                  editor.opts.htmlAllowedAttrs = editor._originalAllowedAttrs;
525                  delete editor._originalAllowedAttrs;
526                }
527              }
528            },
529            fontSize: [
530              '8',
531              '9',
532              '10',
533              '11',
534              '12',
535              '14',
536              '16',
537              '18',
538              '20',
539              '22',
540              '24',
541              '26',
542              '28',
543              '30',
544              '36',
545              '48',
546              '60',
547              '72',
548            ],
549            heightMin: 300,
550            htmlAllowedAttrs: ['alt', 'class', 'dir', 'href', 'rel', 'src', 'style', 'target'],
551            htmlAllowedStyleProps: ['width'],
552            htmlAllowedTags: ['a', 'b', 'br', 'em', 'embed', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'iframe', 'img', 'li', 'ol', 'p', 'strong', 'table', 'tbody', 'td', 'th', 'tr', 'u', 'ul', 'video'],
553            iframe: true,
554            iframeStyleFiles: [
555              '/members/tpl/x/config.css.php',
556              '/members/tpl/x/froala/froala-iframe-styles.css'
557            ],
558            imageInsertButtons: [
559              'imageUpload',
560              'imageByURL'
561            ],
562            imageUploadRemoteUrls: false,
563            key: froalaKey,
564            linkEditButtons: ['linkOpen', 'linkEdit', 'linkRemove'],
565            linkInsertButtons: ['linkBack'],
566            linkStyles: {},
567            paragraphFormat: {
568              N: 'Normal',
569              H1: 'Heading 1',
570              H2: 'Heading 2',
571              H3: 'Heading 3',
572              H4: 'Heading 4',
573              H5: 'Heading 5',
574              H6: 'Heading 6'
575            },
576            pasteAllowedStyleProps: ['direction', 'text-align'],
577            pasteDeniedAttrs: ['style', 'class'],
578            pasteDeniedTags: ['span', 'div'],
579            quickInsertButtons: [
580              'image',
581              'video',
582            ],
583            styleRules: {
584              'direction': ['ltr', 'rtl'],
585              'text-align': ['left', 'right', 'center', 'justify']
586            },
587            tableEditButtons: ['tableRows', 'tableColumns', 'tableHeader', 'tableRemove'],
588            tabSpaces: 4,
589            toolbarButtons: {
590              moreMisc: {
591                align: 'right',
592                buttons: ['undo', 'redo']
593              },
594              moreText: {
595                buttonsVisible: 3,
596                buttons: [
597                  'bold',
598                  'italic',
599                  'underline',
600                  'paragraphFormat',
601                  'clearFormatting',
602                ]
603              },
604              moreParagraph: {
605                buttonsVisible: 2,
606                buttons: [
607                  'formatUL',
608                  'formatOL',
609                  'alignLeft',
610                  'alignCenter',
611                  'alignRight',
612                  'alignJustify',
613                ]
614              },
615              moreRich: {
616                buttons: [
617                  'insertLink',
618                  'insertImage',
619                  'insertVideo',
620                  'insertTable',
621                  'insertFile',
622                ]
623              },
624            },
625            useClasses: true,
626            videoInsertButtons: [
627              'videoByURL',
628              'videoEmbed',
629            ],
630            videoAllowedProviders: [
631              'youtube',
632              'vimeo'
633            ],
634            wordPasteModal: false,
635          };
636
637          // Add the configs for media
638          config = {
639            ...editorConfig,
640            ...imageUploadConfig,
641            ...videoUploadConfig,
642            ...fileUploadConfig,
643          };
644          break;
645          // Add more data-type cases here as needed
646        default:
647          // For unknown data-types, fall through to default config
648          break;
649      }
650    }
651
652    if (!dataType) {
653      config.quickInsertButtons = config.quickInsertButtons || [];
654      config.htmlAllowedTags = htmlAllowedTags;
655      config.htmlAllowedAttrs = [
656        ...htmlAllowedAttrs,
657        'href',
658        'target',
659        'rel',
660        'colspan',
661      ];
662      config.fontSize = [
663        '8',
664        '9',
665        '10',
666        '11',
667        '12',
668        '14',
669        '16',
670        '18',
671        '20',
672        '22',
673        '24',
674        '26',
675        '28',
676        '30',
677        '36',
678        '48',
679        '60',
680        '72',
681      ];
682
683      let allowAll = selector.classList.contains('allowAll');
684
685      if (selector.classList.contains('stripVendorStyle')) {
686        _stripVendorStyle = true;
687      }
688
689      // Configuration specific to the Froala editor
690      if (!selector.classList.contains('disableIframe')) {
691        config.iframe = true; // default iframe behaviour
692        config.iframeStyleFiles = [
693          '/members/tpl/x/config.css.php',
694          '/members/tpl/x/froala/froala-iframe-styles.css'
695        ];
696      }
697
698      if (selector.classList.contains('disableWordPasteStyles')) {
699        config.pasteDeniedAttrs = ['style'];
700        config.wordPasteModal = false;
701      }
702
703      if (selector.classList.contains('disableImagePaste')) {
704        config.imagePaste = false;
705      }
706
707      let toolbarButtons = {};
708      let moreText = {
709        buttonsVisible: 3,
710      };
711
712      // for headings
713      moreText.buttons = [
714        'bold',
715        'italic',
716        'clearFormatting',
717      ];
718      let moreParagraph = {
719        buttonsVisible: 4,
720      };
721      moreParagraph.buttons = [
722        'formatUL'
723      ];
724      let moreRich = {
725        buttons: [],
726      };
727      let moreMisc = {
728        buttons: ['undo', 'redo'],
729        align: 'right',
730      };
731
732      if (selector.classList.contains('allowHeadings') || allowAll) {
733        config.heightMin = 300;
734        config.paragraphFormatSelection = true;
735        config.paragraphDefaultSelection = 'Paragraph';
736        config.paragraphFormat = {
737          N: 'Normal',
738          H1: 'Heading 1',
739          H2: 'Heading 2',
740          H3: 'Heading 3',
741          H4: 'Heading 4',
742          H5: 'Heading 5',
743          H6: 'Heading 6',
744        };
745        moreText.buttons = [
746          ...moreText.buttons,
747          'paragraphFormat'
748        ];
749      }
750
751      if (selector.classList.contains('allowOrderedList') || allowAll) {
752        moreParagraph.buttons = [
753          ...moreParagraph.buttons,
754          'formatOLSimple',
755          'formatOL',
756        ];
757
758        config.quickInsertButtons = [
759          ...config.quickInsertButtons,
760          'ul',
761          'ol',
762        ];
763      }
764
765      if (selector.classList.contains('allowLink') || allowAll) {
766        config.linkInsertButtons = ['linkBack'];
767        config.linkStyles = {};
768        config.linkEditButtons = [
769          'linkOpen',
770          'linkEdit',
771          'linkRemove'
772        ];
773
774        moreRich.buttons = [
775          ...moreRich.buttons,
776          'insertLink'
777        ];
778      }
779
780      if (selector.classList.contains('allowAligning') || allowAll) {
781        moreParagraph.buttons = [
782          ...moreParagraph.buttons,
783          'alignLeft',
784          'alignCenter',
785          'alignRight',
786          'alignJustify',
787        ];
788      }
789
790      if (selector.classList.contains('allowImages') || allowAll) {
791        config = {
792          ...config,
793          ...imageUploadConfig
794        };
795        config.imageInsertButtons = [
796          'imageUpload',
797          'imageByURL'
798        ];
799        config.imageUploadRemoteUrls = false;
800
801        moreRich.buttons = [
802          ...moreRich.buttons,
803          'insertImage'
804        ];
805
806        config.quickInsertButtons = [
807          ...config.quickInsertButtons,
808          'image'
809        ];
810      }
811
812      if (selector.classList.contains('allowVideo') || allowAll) {
813        config = {
814          ...config,
815          ...videoUploadConfig
816        };
817
818        config.videoInsertButtons = [
819          'videoByURL',
820          'videoEmbed',
821          'videoUpload'
822        ];
823        config.videoAllowedProviders = [
824          'youtube',
825          'vimeo'
826        ];
827
828        moreRich.buttons = [
829          ...moreRich.buttons,
830          'insertVideo'
831        ];
832
833        config.quickInsertButtons = [
834          ...config.quickInsertButtons,
835          'video'
836        ];
837      }
838
839      if (selector.classList.contains('allowUnderline') || allowAll) {
840        moreText.buttons = [
841          ...moreText.buttons,
842          'underline',
843        ];
844      }
845
846      if (selector.classList.contains('allowColour') || allowAll) {
847        moreText.buttons = [
848          ...moreText.buttons,
849          'textColor',
850          'backgroundColor',
851        ];
852      }
853
854      if (selector.classList.contains('allowPasteOptions') || allowAll) {
855        moreRich.buttons = [
856          ...moreRich.buttons,
857          'paste', 'pastePlain', 'pasteFromWord'
858        ];
859      }
860
861      if (allowAll) {
862        moreText.buttons = [
863          ...moreText.buttons,
864          'strikethrough',
865          'subscript',
866          'superscript',
867          'fontSize',
868          'fontFamily',
869          'inlineStyle',
870        ];
871
872        moreParagraph.buttons = [
873          ...moreParagraph.buttons,
874          'outdent',
875          'indent',
876          'quote'
877        ];
878
879        moreRich.buttons = [
880          ...moreRich.buttons,
881          'insertTable',
882          'insertFile',
883        ];
884
885        config.quickInsertButtons = [
886          ...config.quickInsertButtons,
887          'table',
888          'hr',
889        ];
890
891        config = {
892          ...config,
893          ...fileUploadConfig
894        };
895      }
896
897      toolbarButtons.moreText = moreText;
898      toolbarButtons.moreParagraph = moreParagraph;
899      toolbarButtons.moreRich = moreRich;
900      toolbarButtons.moreMisc = moreMisc;
901      config.toolbarButtons = toolbarButtons;
902    }
903
904    config.quickInsertEnabled = config.quickInsertButtons && config.quickInsertButtons.length > 0;
905  }
906  return {...config, ...options};
907}

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.