| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  | // Copyright 2020 Signal Messenger, LLC
 | 
					
						
							|  |  |  | // SPDX-License-Identifier: AGPL-3.0-only
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import type Quill from 'quill'; | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  | import type { KeyboardContext } from 'quill'; | 
					
						
							| 
									
										
										
										
											2023-06-08 14:50:44 -07:00
										 |  |  | import type Op from 'quill-delta/dist/Op'; | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  | import React from 'react'; | 
					
						
							|  |  |  | import classNames from 'classnames'; | 
					
						
							|  |  |  | import { Popper } from 'react-popper'; | 
					
						
							|  |  |  | import { createPortal } from 'react-dom'; | 
					
						
							|  |  |  | import type { VirtualElement } from '@popperjs/core'; | 
					
						
							| 
									
										
										
										
											2023-06-08 14:50:44 -07:00
										 |  |  | import { isString } from 'lodash'; | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | import * as log from '../../logging/log'; | 
					
						
							|  |  |  | import * as Errors from '../../types/errors'; | 
					
						
							|  |  |  | import type { LocalizerType } from '../../types/Util'; | 
					
						
							|  |  |  | import { handleOutsideClick } from '../../util/handleOutsideClick'; | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-04 09:25:52 -07:00
										 |  |  | const MENU_FADE_OUT_MS = 200; | 
					
						
							|  |  |  | const POPUP_GUIDE_FADE_MS = 120; | 
					
						
							|  |  |  | const BUTTON_HOVER_TIMEOUT_MS = 900; | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  | const MENU_TEXT_BUFFER = 12; // pixels
 | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | // Note: Keyboard shortcuts are defined in the constructor below, and when using
 | 
					
						
							|  |  |  | //   <FormattingButton /> below. They're also referenced in ShortcutGuide.tsx.
 | 
					
						
							|  |  |  | const BOLD_CHAR = 'B'; | 
					
						
							|  |  |  | const ITALIC_CHAR = 'I'; | 
					
						
							|  |  |  | const MONOSPACE_CHAR = 'E'; | 
					
						
							|  |  |  | const SPOILER_CHAR = 'B'; | 
					
						
							|  |  |  | const STRIKETHROUGH_CHAR = 'X'; | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | type FormattingPickerOptions = { | 
					
						
							|  |  |  |   i18n: LocalizerType; | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |   isMenuEnabled: boolean; | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |   isMouseDown?: boolean; | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |   isEnabled: boolean; | 
					
						
							|  |  |  |   isSpoilersEnabled: boolean; | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |   platform: string; | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |   setFormattingChooserElement: (element: JSX.Element | null) => void; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export enum QuillFormattingStyle { | 
					
						
							|  |  |  |   bold = 'bold', | 
					
						
							|  |  |  |   italic = 'italic', | 
					
						
							|  |  |  |   monospace = 'monospace', | 
					
						
							|  |  |  |   strike = 'strike', | 
					
						
							|  |  |  |   spoiler = 'spoiler', | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  | function getMetaKey(platform: string, i18n: LocalizerType) { | 
					
						
							|  |  |  |   const isMacOS = platform === 'darwin'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (isMacOS) { | 
					
						
							|  |  |  |     return '⌘'; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return i18n('icu:Keyboard--Key--ctrl'); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-08 14:50:44 -07:00
										 |  |  | function isAllNewlines(ops: Array<Op>): boolean { | 
					
						
							|  |  |  |   return ops.every(isNewlineOnlyOp); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export function isNewlineOnlyOp(op: Op): boolean { | 
					
						
							| 
									
										
										
										
											2023-06-12 12:22:56 -07:00
										 |  |  |   return isString(op.insert) && /^\n+$/g.test(op.insert); | 
					
						
							| 
									
										
										
										
											2023-06-08 14:50:44 -07:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  | export class FormattingMenu { | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |   // Cache the results of our virtual elements's last rect calculation
 | 
					
						
							|  |  |  |   lastRect: DOMRect | undefined; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Keep a references to our originally passed (or updated) options
 | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |   options: FormattingPickerOptions; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |   // Used to dismiss our menu if we click outside it
 | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |   outsideClickDestructor?: () => void; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |   // Maintaining a direct reference to quill
 | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |   quill: Quill; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |   // The element we hand to Popper to position the menu
 | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |   referenceElement: VirtualElement | undefined; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |   // The host for our portal
 | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |   root: HTMLDivElement; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |   // Timer to track an animated fade-out, then DOM removal
 | 
					
						
							|  |  |  |   fadingOutTimeout?: NodeJS.Timeout; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |   constructor(quill: Quill, options: FormattingPickerOptions) { | 
					
						
							|  |  |  |     this.quill = quill; | 
					
						
							|  |  |  |     this.options = options; | 
					
						
							|  |  |  |     this.root = document.body.appendChild(document.createElement('div')); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     this.quill.on('editor-change', this.onEditorChange.bind(this)); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-17 18:16:41 -07:00
										 |  |  |     // We override these keybindings, which means that we need to move their priority
 | 
					
						
							|  |  |  |     //   above the built-in shortcuts, which don't exactly do what we want.
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |     const boldCharCode = BOLD_CHAR.charCodeAt(0); | 
					
						
							|  |  |  |     this.quill.keyboard.addBinding( | 
					
						
							|  |  |  |       { key: BOLD_CHAR, shortKey: true }, | 
					
						
							|  |  |  |       (_range, context) => | 
					
						
							|  |  |  |         this.toggleForStyle(QuillFormattingStyle.bold, context) | 
					
						
							| 
									
										
										
										
											2023-04-17 18:16:41 -07:00
										 |  |  |     ); | 
					
						
							|  |  |  |     quill.keyboard.bindings[boldCharCode].unshift( | 
					
						
							|  |  |  |       quill.keyboard.bindings[boldCharCode].pop() | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |     const italicCharCode = ITALIC_CHAR.charCodeAt(0); | 
					
						
							|  |  |  |     this.quill.keyboard.addBinding( | 
					
						
							|  |  |  |       { key: ITALIC_CHAR, shortKey: true }, | 
					
						
							|  |  |  |       (_range, context) => | 
					
						
							|  |  |  |         this.toggleForStyle(QuillFormattingStyle.italic, context) | 
					
						
							| 
									
										
										
										
											2023-04-17 18:16:41 -07:00
										 |  |  |     ); | 
					
						
							|  |  |  |     quill.keyboard.bindings[italicCharCode].unshift( | 
					
						
							|  |  |  |       quill.keyboard.bindings[italicCharCode].pop() | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // No need for changing priority for these new keybindings
 | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |     this.quill.keyboard.addBinding( | 
					
						
							|  |  |  |       { key: MONOSPACE_CHAR, shortKey: true }, | 
					
						
							|  |  |  |       (_range, context) => | 
					
						
							|  |  |  |         this.toggleForStyle(QuillFormattingStyle.monospace, context) | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |     ); | 
					
						
							|  |  |  |     this.quill.keyboard.addBinding( | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |       { key: STRIKETHROUGH_CHAR, shortKey: true, shiftKey: true }, | 
					
						
							|  |  |  |       (_range, context) => | 
					
						
							|  |  |  |         this.toggleForStyle(QuillFormattingStyle.strike, context) | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |     ); | 
					
						
							|  |  |  |     this.quill.keyboard.addBinding( | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |       { key: SPOILER_CHAR, shortKey: true, shiftKey: true }, | 
					
						
							|  |  |  |       (_range, context) => | 
					
						
							|  |  |  |         this.toggleForStyle(QuillFormattingStyle.spoiler, context) | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   destroy(): void { | 
					
						
							|  |  |  |     this.root.remove(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   updateOptions(options: Partial<FormattingPickerOptions>): void { | 
					
						
							|  |  |  |     this.options = { ...this.options, ...options }; | 
					
						
							|  |  |  |     this.onEditorChange(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |   scheduleRemoval(): void { | 
					
						
							| 
									
										
										
										
											2023-05-11 10:02:36 -07:00
										 |  |  |     // Nothing to do
 | 
					
						
							|  |  |  |     if (!this.referenceElement) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Already scheduled
 | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |     if (this.fadingOutTimeout) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     this.fadingOutTimeout = setTimeout(() => { | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |       this.referenceElement = undefined; | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |       this.lastRect = undefined; | 
					
						
							|  |  |  |       this.fadingOutTimeout = undefined; | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |       this.render(); | 
					
						
							| 
									
										
										
										
											2023-08-04 09:25:52 -07:00
										 |  |  |     }, MENU_FADE_OUT_MS); | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     this.render(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   cancelRemoval(): void { | 
					
						
							|  |  |  |     if (this.fadingOutTimeout) { | 
					
						
							|  |  |  |       clearTimeout(this.fadingOutTimeout); | 
					
						
							|  |  |  |       this.fadingOutTimeout = undefined; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |   onEditorChange(): void { | 
					
						
							| 
									
										
										
										
											2023-05-11 10:02:36 -07:00
										 |  |  |     if (!this.options.isMenuEnabled || !this.options.isEnabled) { | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |       this.scheduleRemoval(); | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const isFocused = this.quill.hasFocus(); | 
					
						
							|  |  |  |     if (!isFocused) { | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |       this.scheduleRemoval(); | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const quillSelection = this.quill.getSelection(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |     if (!quillSelection || quillSelection.length === 0) { | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |       this.scheduleRemoval(); | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-08 14:50:44 -07:00
										 |  |  |     // Note: we special-case all-newline ops because Quill doesn't apply styles to them
 | 
					
						
							| 
									
										
										
										
											2023-05-17 09:58:32 -07:00
										 |  |  |     const contents = this.quill.getContents( | 
					
						
							|  |  |  |       quillSelection.index, | 
					
						
							|  |  |  |       quillSelection.length | 
					
						
							|  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2023-06-08 14:50:44 -07:00
										 |  |  |     if (isAllNewlines(contents.ops)) { | 
					
						
							| 
									
										
										
										
											2023-05-17 09:58:32 -07:00
										 |  |  |       this.scheduleRemoval(); | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |     // a virtual reference to the text we are trying to format
 | 
					
						
							|  |  |  |     this.cancelRemoval(); | 
					
						
							|  |  |  |     this.referenceElement = { | 
					
						
							|  |  |  |       getBoundingClientRect: () => { | 
					
						
							|  |  |  |         const selection = window.getSelection(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // there's a selection and at least one range
 | 
					
						
							|  |  |  |         if (selection != null && selection.rangeCount !== 0) { | 
					
						
							|  |  |  |           // grab the first range, the one the user is actually on right now
 | 
					
						
							|  |  |  |           const range = selection.getRangeAt(0); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           const { activeElement } = document; | 
					
						
							|  |  |  |           const editorElement = activeElement?.closest( | 
					
						
							|  |  |  |             '.module-composition-input__input' | 
					
						
							|  |  |  |           ); | 
					
						
							|  |  |  |           const editorRect = editorElement?.getClientRects()[0]; | 
					
						
							|  |  |  |           if (!editorRect) { | 
					
						
							|  |  |  |             // Note: this will happen when a user dismisses a panel; and if we don't
 | 
					
						
							|  |  |  |             //   cache here, the formatting menu will show in the very top-left
 | 
					
						
							|  |  |  |             if (this.lastRect) { | 
					
						
							|  |  |  |               return this.lastRect; | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |             } | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |             log.warn('No editor rect when showing formatting menu'); | 
					
						
							|  |  |  |             return new DOMRect(); | 
					
						
							|  |  |  |           } | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |           const rect = range.getBoundingClientRect(); | 
					
						
							|  |  |  |           if (!rect) { | 
					
						
							|  |  |  |             if (this.lastRect) { | 
					
						
							|  |  |  |               return this.lastRect; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             log.warn('No maximum rect when showing formatting menu'); | 
					
						
							|  |  |  |             return new DOMRect(); | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |           } | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |           // If we've scrolled down and the top of the composer text is invisible, above
 | 
					
						
							|  |  |  |           //   where the editor ends, we fix the popover so it stays connected to the
 | 
					
						
							|  |  |  |           //   visible editor. Important for the 'Cmd-A' scenario when scrolled down.
 | 
					
						
							|  |  |  |           const updatedY = Math.max( | 
					
						
							|  |  |  |             (editorRect.y || 0) - MENU_TEXT_BUFFER, | 
					
						
							|  |  |  |             (rect.y || 0) - MENU_TEXT_BUFFER | 
					
						
							|  |  |  |           ); | 
					
						
							|  |  |  |           const updatedHeight = rect.height + (rect.y - updatedY); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           this.lastRect = DOMRect.fromRect({ | 
					
						
							|  |  |  |             x: rect.x, | 
					
						
							|  |  |  |             y: updatedY, | 
					
						
							|  |  |  |             height: updatedHeight, | 
					
						
							|  |  |  |             width: rect.width, | 
					
						
							|  |  |  |           }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           return this.lastRect; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         log.warn('No selection range when showing formatting menu'); | 
					
						
							|  |  |  |         return new DOMRect(); | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |     }; | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     this.render(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   isStyleEnabledInSelection(style: QuillFormattingStyle): boolean | undefined { | 
					
						
							|  |  |  |     const selection = this.quill.getSelection(); | 
					
						
							|  |  |  |     if (!selection || !selection.length) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     const contents = this.quill.getContents(selection.index, selection.length); | 
					
						
							| 
									
										
										
										
											2023-05-12 13:48:14 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // Note: we special-case single \n ops because Quill doesn't apply formatting to them
 | 
					
						
							| 
									
										
										
										
											2023-06-08 14:50:44 -07:00
										 |  |  |     if (isAllNewlines(contents.ops)) { | 
					
						
							| 
									
										
										
										
											2023-05-17 09:58:32 -07:00
										 |  |  |       return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-12 13:48:14 -07:00
										 |  |  |     return contents.ops.every( | 
					
						
							| 
									
										
										
										
											2023-06-08 14:50:44 -07:00
										 |  |  |       op => op.attributes?.[style] || isNewlineOnlyOp(op) | 
					
						
							| 
									
										
										
										
											2023-05-12 13:48:14 -07:00
										 |  |  |     ); | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |   toggleForStyle(style: QuillFormattingStyle, context?: KeyboardContext): void { | 
					
						
							|  |  |  |     if (!this.options.isEnabled) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if ( | 
					
						
							|  |  |  |       !this.options.isSpoilersEnabled && | 
					
						
							|  |  |  |       style === QuillFormattingStyle.spoiler | 
					
						
							|  |  |  |     ) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |     try { | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |       const isEnabled = context | 
					
						
							|  |  |  |         ? Boolean(context.format[style]) | 
					
						
							|  |  |  |         : this.isStyleEnabledInSelection(style); | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |       if (isEnabled === undefined) { | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       this.quill.format(style, !isEnabled); | 
					
						
							|  |  |  |     } catch (error) { | 
					
						
							|  |  |  |       log.error('toggleForStyle error:', Errors.toLogFormat(error)); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   render(): void { | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |     if (!this.referenceElement) { | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |       this.outsideClickDestructor?.(); | 
					
						
							|  |  |  |       this.outsideClickDestructor = undefined; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       this.options.setFormattingChooserElement(null); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |     const { i18n, isSpoilersEnabled, platform } = this.options; | 
					
						
							|  |  |  |     const metaKey = getMetaKey(platform, i18n); | 
					
						
							|  |  |  |     const shiftKey = i18n('icu:Keyboard--Key--shift'); | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // showing the popup format menu
 | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |     const isStyleEnabledInSelection = this.isStyleEnabledInSelection.bind(this); | 
					
						
							|  |  |  |     const toggleForStyle = this.toggleForStyle.bind(this); | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |     const element = createPortal( | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |       <Popper placement="top" referenceElement={this.referenceElement}> | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |         {({ ref, style }) => { | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |           const opacity = | 
					
						
							|  |  |  |             style.transform && | 
					
						
							|  |  |  |             !this.options.isMouseDown && | 
					
						
							|  |  |  |             !this.fadingOutTimeout | 
					
						
							|  |  |  |               ? 1 | 
					
						
							|  |  |  |               : 0; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |           const [hasLongHovered, setHasLongHovered] = | 
					
						
							|  |  |  |             React.useState<boolean>(false); | 
					
						
							|  |  |  |           const onLongHover = React.useCallback( | 
					
						
							|  |  |  |             (value: boolean) => { | 
					
						
							|  |  |  |               setHasLongHovered(value); | 
					
						
							|  |  |  |             }, | 
					
						
							|  |  |  |             [setHasLongHovered] | 
					
						
							|  |  |  |           ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           return ( | 
					
						
							|  |  |  |             <div | 
					
						
							|  |  |  |               ref={ref} | 
					
						
							|  |  |  |               className="module-composition-input__format-menu" | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |               style={{ ...style, opacity }} | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |               role="menu" | 
					
						
							|  |  |  |               tabIndex={0} | 
					
						
							|  |  |  |               onMouseLeave={() => setHasLongHovered(false)} | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |             > | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |               <FormattingButton | 
					
						
							|  |  |  |                 hasLongHovered={hasLongHovered} | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |                 isActive={isStyleEnabledInSelection(QuillFormattingStyle.bold)} | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |                 label={i18n('icu:Keyboard--composer--bold')} | 
					
						
							|  |  |  |                 onLongHover={onLongHover} | 
					
						
							|  |  |  |                 popupGuideShortcut={`${metaKey} + ${BOLD_CHAR}`} | 
					
						
							|  |  |  |                 popupGuideText={i18n('icu:FormatMenu--guide--bold')} | 
					
						
							|  |  |  |                 style={QuillFormattingStyle.bold} | 
					
						
							|  |  |  |                 toggleForStyle={toggleForStyle} | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |               /> | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |               <FormattingButton | 
					
						
							|  |  |  |                 hasLongHovered={hasLongHovered} | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |                 isActive={isStyleEnabledInSelection( | 
					
						
							|  |  |  |                   QuillFormattingStyle.italic | 
					
						
							|  |  |  |                 )} | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |                 label={i18n('icu:Keyboard--composer--italic')} | 
					
						
							|  |  |  |                 onLongHover={onLongHover} | 
					
						
							|  |  |  |                 popupGuideShortcut={`${metaKey} + ${ITALIC_CHAR}`} | 
					
						
							|  |  |  |                 popupGuideText={i18n('icu:FormatMenu--guide--italic')} | 
					
						
							|  |  |  |                 style={QuillFormattingStyle.italic} | 
					
						
							|  |  |  |                 toggleForStyle={toggleForStyle} | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |               /> | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |               <FormattingButton | 
					
						
							|  |  |  |                 hasLongHovered={hasLongHovered} | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |                 isActive={isStyleEnabledInSelection( | 
					
						
							|  |  |  |                   QuillFormattingStyle.strike | 
					
						
							|  |  |  |                 )} | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |                 label={i18n('icu:Keyboard--composer--strikethrough')} | 
					
						
							|  |  |  |                 onLongHover={onLongHover} | 
					
						
							|  |  |  |                 popupGuideShortcut={`${metaKey} + ${shiftKey} + ${STRIKETHROUGH_CHAR}`} | 
					
						
							|  |  |  |                 popupGuideText={i18n('icu:FormatMenu--guide--strikethrough')} | 
					
						
							|  |  |  |                 style={QuillFormattingStyle.strike} | 
					
						
							|  |  |  |                 toggleForStyle={toggleForStyle} | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |               /> | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |               <FormattingButton | 
					
						
							|  |  |  |                 hasLongHovered={hasLongHovered} | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |                 isActive={isStyleEnabledInSelection( | 
					
						
							|  |  |  |                   QuillFormattingStyle.monospace | 
					
						
							|  |  |  |                 )} | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |                 label={i18n('icu:Keyboard--composer--monospace')} | 
					
						
							|  |  |  |                 onLongHover={onLongHover} | 
					
						
							|  |  |  |                 popupGuideShortcut={`${metaKey} + ${MONOSPACE_CHAR}`} | 
					
						
							|  |  |  |                 popupGuideText={i18n('icu:FormatMenu--guide--monospace')} | 
					
						
							|  |  |  |                 style={QuillFormattingStyle.monospace} | 
					
						
							|  |  |  |                 toggleForStyle={toggleForStyle} | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |               /> | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |               {isSpoilersEnabled ? ( | 
					
						
							|  |  |  |                 <FormattingButton | 
					
						
							|  |  |  |                   hasLongHovered={hasLongHovered} | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |                   isActive={isStyleEnabledInSelection( | 
					
						
							|  |  |  |                     QuillFormattingStyle.spoiler | 
					
						
							|  |  |  |                   )} | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |                   onLongHover={onLongHover} | 
					
						
							|  |  |  |                   popupGuideShortcut={`${metaKey} + ${shiftKey} + ${SPOILER_CHAR}`} | 
					
						
							|  |  |  |                   popupGuideText={i18n('icu:FormatMenu--guide--spoiler')} | 
					
						
							|  |  |  |                   label={i18n('icu:Keyboard--composer--spoiler')} | 
					
						
							|  |  |  |                   style={QuillFormattingStyle.spoiler} | 
					
						
							|  |  |  |                   toggleForStyle={toggleForStyle} | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |                 /> | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |               ) : null} | 
					
						
							|  |  |  |             </div> | 
					
						
							|  |  |  |           ); | 
					
						
							|  |  |  |         }} | 
					
						
							| 
									
										
										
										
											2023-04-14 11:16:28 -07:00
										 |  |  |       </Popper>, | 
					
						
							|  |  |  |       this.root | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Just to make sure that we don't propagate outside clicks until this is closed.
 | 
					
						
							|  |  |  |     this.outsideClickDestructor?.(); | 
					
						
							|  |  |  |     this.outsideClickDestructor = handleOutsideClick( | 
					
						
							|  |  |  |       () => { | 
					
						
							|  |  |  |         return true; | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |       { | 
					
						
							|  |  |  |         name: 'quill.emoji.completion', | 
					
						
							|  |  |  |         containerElements: [this.root], | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     this.options.setFormattingChooserElement(element); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | function FormattingButton({ | 
					
						
							|  |  |  |   hasLongHovered, | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |   isActive, | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |   label, | 
					
						
							|  |  |  |   onLongHover, | 
					
						
							|  |  |  |   popupGuideText, | 
					
						
							|  |  |  |   popupGuideShortcut, | 
					
						
							|  |  |  |   style, | 
					
						
							|  |  |  |   toggleForStyle, | 
					
						
							|  |  |  | }: { | 
					
						
							|  |  |  |   hasLongHovered: boolean; | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |   isActive: boolean | undefined; | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |   label: string; | 
					
						
							|  |  |  |   onLongHover: (value: boolean) => unknown; | 
					
						
							|  |  |  |   popupGuideText: string; | 
					
						
							|  |  |  |   popupGuideShortcut: string; | 
					
						
							|  |  |  |   style: QuillFormattingStyle; | 
					
						
							|  |  |  |   toggleForStyle: (style: QuillFormattingStyle) => unknown; | 
					
						
							|  |  |  | }): JSX.Element { | 
					
						
							|  |  |  |   const buttonRef = React.useRef<HTMLButtonElement | null>(null); | 
					
						
							| 
									
										
										
										
											2023-08-04 09:25:52 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |   const [isHovered, setIsHovered] = React.useState<boolean>(false); | 
					
						
							| 
									
										
										
										
											2023-08-04 09:25:52 -07:00
										 |  |  |   const hoverTimerRef = React.useRef<NodeJS.Timeout | undefined>(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const [isFadingOut, setIsFadingOut] = React.useState<boolean>(false); | 
					
						
							|  |  |  |   const fadeOutTimerRef = React.useRef<NodeJS.Timeout | undefined>(); | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |   React.useEffect(() => { | 
					
						
							|  |  |  |     return () => { | 
					
						
							| 
									
										
										
										
											2023-08-04 09:25:52 -07:00
										 |  |  |       if (hoverTimerRef.current) { | 
					
						
							|  |  |  |         clearTimeout(hoverTimerRef.current); | 
					
						
							|  |  |  |         hoverTimerRef.current = undefined; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (fadeOutTimerRef.current) { | 
					
						
							|  |  |  |         clearTimeout(fadeOutTimerRef.current); | 
					
						
							|  |  |  |         fadeOutTimerRef.current = undefined; | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |       } | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   }, []); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |   return ( | 
					
						
							|  |  |  |     <> | 
					
						
							| 
									
										
										
										
											2023-08-04 09:25:52 -07:00
										 |  |  |       {(isFadingOut || (hasLongHovered && isHovered)) && buttonRef.current ? ( | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |         <Popper placement="top" referenceElement={buttonRef.current}> | 
					
						
							| 
									
										
										
										
											2023-08-04 09:25:52 -07:00
										 |  |  |           {({ ref, style: popperStyles }) => { | 
					
						
							|  |  |  |             const opacity = !popperStyles.transform || isFadingOut ? 0 : 1; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             return ( | 
					
						
							|  |  |  |               <div | 
					
						
							|  |  |  |                 className="module-composition-input__format-menu__item__popover" | 
					
						
							|  |  |  |                 ref={ref} | 
					
						
							|  |  |  |                 style={{ ...popperStyles, opacity }} | 
					
						
							|  |  |  |               > | 
					
						
							|  |  |  |                 {popupGuideText} | 
					
						
							|  |  |  |                 <div className="module-composition-input__format-menu__item__popover__shortcut"> | 
					
						
							|  |  |  |                   {popupGuideShortcut} | 
					
						
							|  |  |  |                 </div> | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |               </div> | 
					
						
							| 
									
										
										
										
											2023-08-04 09:25:52 -07:00
										 |  |  |             ); | 
					
						
							|  |  |  |           }} | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |         </Popper> | 
					
						
							|  |  |  |       ) : null} | 
					
						
							|  |  |  |       <button | 
					
						
							|  |  |  |         ref={buttonRef} | 
					
						
							|  |  |  |         type="button" | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |         className={classNames( | 
					
						
							|  |  |  |           'module-composition-input__format-menu__item', | 
					
						
							|  |  |  |           isActive | 
					
						
							|  |  |  |             ? 'module-composition-input__format-menu__item--active' | 
					
						
							|  |  |  |             : null | 
					
						
							|  |  |  |         )} | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |         aria-label={label} | 
					
						
							|  |  |  |         onClick={event => { | 
					
						
							|  |  |  |           event.preventDefault(); | 
					
						
							|  |  |  |           event.stopPropagation(); | 
					
						
							|  |  |  |           onLongHover(false); | 
					
						
							|  |  |  |           toggleForStyle(style); | 
					
						
							|  |  |  |         }} | 
					
						
							|  |  |  |         onMouseEnter={() => { | 
					
						
							| 
									
										
										
										
											2023-08-04 09:25:52 -07:00
										 |  |  |           if (hoverTimerRef.current) { | 
					
						
							|  |  |  |             clearTimeout(hoverTimerRef.current); | 
					
						
							|  |  |  |             hoverTimerRef.current = undefined; | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |           } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-04 09:25:52 -07:00
										 |  |  |           hoverTimerRef.current = setTimeout(() => { | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |             onLongHover(true); | 
					
						
							| 
									
										
										
										
											2023-08-04 09:25:52 -07:00
										 |  |  |           }, BUTTON_HOVER_TIMEOUT_MS); | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |           setIsHovered(true); | 
					
						
							|  |  |  |         }} | 
					
						
							|  |  |  |         onMouseLeave={() => { | 
					
						
							| 
									
										
										
										
											2023-08-04 09:25:52 -07:00
										 |  |  |           if (hoverTimerRef.current) { | 
					
						
							|  |  |  |             clearTimeout(hoverTimerRef.current); | 
					
						
							|  |  |  |             hoverTimerRef.current = undefined; | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           if (hasLongHovered && isHovered) { | 
					
						
							|  |  |  |             fadeOutTimerRef.current = setTimeout(() => { | 
					
						
							|  |  |  |               setIsFadingOut(false); | 
					
						
							|  |  |  |               fadeOutTimerRef.current = undefined; | 
					
						
							|  |  |  |             }, POPUP_GUIDE_FADE_MS); | 
					
						
							|  |  |  |             setIsFadingOut(true); | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |           } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           setIsHovered(false); | 
					
						
							|  |  |  |         }} | 
					
						
							|  |  |  |       > | 
					
						
							|  |  |  |         <div | 
					
						
							|  |  |  |           className={classNames( | 
					
						
							|  |  |  |             'module-composition-input__format-menu__item__icon', | 
					
						
							|  |  |  |             `module-composition-input__format-menu__item__icon--${style}`, | 
					
						
							| 
									
										
										
										
											2023-05-09 18:23:56 -07:00
										 |  |  |             isActive | 
					
						
							| 
									
										
										
										
											2023-05-09 17:40:19 -07:00
										 |  |  |               ? 'module-composition-input__format-menu__item__icon--active' | 
					
						
							|  |  |  |               : null | 
					
						
							|  |  |  |           )} | 
					
						
							|  |  |  |         /> | 
					
						
							|  |  |  |       </button> | 
					
						
							|  |  |  |     </> | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | } |