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.