Merge pull request #17359 from calixteman/editor_highlight_color_picker
[Editor] Add a color picker with predefined colors for highlighting text (bug 1866434)
This commit is contained in:
		
						commit
						8702e1bbb2
					
				| @ -85,6 +85,10 @@ | ||||
|       "type": "boolean", | ||||
|       "default": false | ||||
|     }, | ||||
|     "highlightEditorColors": { | ||||
|       "type": "string", | ||||
|       "default": "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F" | ||||
|     }, | ||||
|     "disableRange": { | ||||
|       "title": "Disable range requests", | ||||
|       "description": "Whether to disable range requests (not recommended).", | ||||
|  | ||||
| @ -1079,6 +1079,7 @@ function buildComponents(defines, dir) { | ||||
|     "web/images/loading-icon.gif", | ||||
|     "web/images/altText_*.svg", | ||||
|     "web/images/editor-toolbar-*.svg", | ||||
|     "web/images/toolbarButton-menuArrow.svg", | ||||
|   ]; | ||||
| 
 | ||||
|   return merge([ | ||||
|  | ||||
| @ -332,6 +332,8 @@ pdfjs-editor-remove-freetext-button = | ||||
|     .title = Remove text | ||||
| pdfjs-editor-remove-stamp-button = | ||||
|     .title = Remove image | ||||
| pdfjs-editor-remove-highlight-button = | ||||
|     .title = Remove highlight | ||||
| 
 | ||||
| ## | ||||
| 
 | ||||
| @ -384,3 +386,23 @@ pdfjs-editor-resizer-label-bottom-right = Bottom right corner — resize | ||||
| pdfjs-editor-resizer-label-bottom-middle = Bottom middle — resize | ||||
| pdfjs-editor-resizer-label-bottom-left = Bottom left corner — resize | ||||
| pdfjs-editor-resizer-label-middle-left = Middle left — resize | ||||
| 
 | ||||
| ## Color picker | ||||
| 
 | ||||
| # This means "Color used to highlight text" | ||||
| pdfjs-editor-highlight-colorpicker-label = Highlight color | ||||
| 
 | ||||
| pdfjs-editor-colorpicker-button = | ||||
|     .title = Change color | ||||
| pdfjs-editor-colorpicker-dropdown = | ||||
|     .aria-label = Color choices | ||||
| pdfjs-editor-colorpicker-yellow = | ||||
|     .title = Yellow | ||||
| pdfjs-editor-colorpicker-green = | ||||
|     .title = Green | ||||
| pdfjs-editor-colorpicker-blue = | ||||
|     .title = Blue | ||||
| pdfjs-editor-colorpicker-pink = | ||||
|     .title = Pink | ||||
| pdfjs-editor-colorpicker-red = | ||||
|     .title = Red | ||||
|  | ||||
							
								
								
									
										230
									
								
								src/display/editor/color_picker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										230
									
								
								src/display/editor/color_picker.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,230 @@ | ||||
| /* Copyright 2023 Mozilla Foundation | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| import { AnnotationEditorParamsType, shadow } from "../../shared/util.js"; | ||||
| import { KeyboardManager } from "./tools.js"; | ||||
| import { noContextMenu } from "../display_utils.js"; | ||||
| 
 | ||||
| class ColorPicker { | ||||
|   #boundKeyDown = this.#keyDown.bind(this); | ||||
| 
 | ||||
|   #button = null; | ||||
| 
 | ||||
|   #buttonSwatch = null; | ||||
| 
 | ||||
|   #defaultColor; | ||||
| 
 | ||||
|   #dropdown = null; | ||||
| 
 | ||||
|   #dropdownWasFromKeyboard = false; | ||||
| 
 | ||||
|   #isMainColorPicker = false; | ||||
| 
 | ||||
|   #eventBus; | ||||
| 
 | ||||
|   #uiManager = null; | ||||
| 
 | ||||
|   static get _keyboardManager() { | ||||
|     return shadow( | ||||
|       this, | ||||
|       "_keyboardManager", | ||||
|       new KeyboardManager([ | ||||
|         [ | ||||
|           ["Escape", "mac+Escape"], | ||||
|           ColorPicker.prototype._hideDropdownFromKeyboard, | ||||
|         ], | ||||
|         [[" ", "mac+ "], ColorPicker.prototype._colorSelectFromKeyboard], | ||||
|         [ | ||||
|           ["ArrowDown", "ArrowRight", "mac+ArrowDown", "mac+ArrowRight"], | ||||
|           ColorPicker.prototype._moveToNext, | ||||
|         ], | ||||
|         [ | ||||
|           ["ArrowUp", "ArrowLeft", "mac+ArrowUp", "mac+ArrowLeft"], | ||||
|           ColorPicker.prototype._moveToPrevious, | ||||
|         ], | ||||
|         [["Home", "mac+Home"], ColorPicker.prototype._moveToBeginning], | ||||
|         [["End", "mac+End"], ColorPicker.prototype._moveToEnd], | ||||
|       ]) | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   constructor({ editor = null, uiManager = null }) { | ||||
|     this.#isMainColorPicker = !editor; | ||||
|     this.#uiManager = editor?._uiManager || uiManager; | ||||
|     this.#eventBus = this.#uiManager._eventBus; | ||||
|     this.#defaultColor = | ||||
|       editor?.color || | ||||
|       this.#uiManager?.highlightColors.values().next().value || | ||||
|       "#FFFF98"; | ||||
|   } | ||||
| 
 | ||||
|   renderButton() { | ||||
|     const button = (this.#button = document.createElement("button")); | ||||
|     button.className = "colorPicker"; | ||||
|     button.tabIndex = "0"; | ||||
|     button.setAttribute("data-l10n-id", "pdfjs-editor-colorpicker-button"); | ||||
|     button.setAttribute("aria-haspopup", true); | ||||
|     button.addEventListener("click", this.#openDropdown.bind(this)); | ||||
|     const swatch = (this.#buttonSwatch = document.createElement("span")); | ||||
|     swatch.className = "swatch"; | ||||
|     swatch.style.backgroundColor = this.#defaultColor; | ||||
|     button.append(swatch); | ||||
|     return button; | ||||
|   } | ||||
| 
 | ||||
|   renderMainDropdown() { | ||||
|     const dropdown = (this.#dropdown = this.#getDropdownRoot( | ||||
|       AnnotationEditorParamsType.HIGHLIGHT_DEFAULT_COLOR | ||||
|     )); | ||||
|     dropdown.setAttribute("aria-orientation", "horizontal"); | ||||
|     dropdown.setAttribute("aria-labelledby", "highlightColorPickerLabel"); | ||||
| 
 | ||||
|     return dropdown; | ||||
|   } | ||||
| 
 | ||||
|   #getDropdownRoot(paramType) { | ||||
|     const div = document.createElement("div"); | ||||
|     div.addEventListener("contextmenu", noContextMenu); | ||||
|     div.className = "dropdown"; | ||||
|     div.role = "listbox"; | ||||
|     div.setAttribute("aria-multiselectable", false); | ||||
|     div.setAttribute("aria-orientation", "vertical"); | ||||
|     div.setAttribute("data-l10n-id", "pdfjs-editor-colorpicker-dropdown"); | ||||
|     for (const [name, color] of this.#uiManager.highlightColors) { | ||||
|       const button = document.createElement("button"); | ||||
|       button.tabIndex = "0"; | ||||
|       button.role = "option"; | ||||
|       button.setAttribute("data-color", color); | ||||
|       button.title = name; | ||||
|       button.setAttribute("data-l10n-id", `pdfjs-editor-colorpicker-${name}`); | ||||
|       const swatch = document.createElement("span"); | ||||
|       button.append(swatch); | ||||
|       swatch.className = "swatch"; | ||||
|       swatch.style.backgroundColor = color; | ||||
|       button.setAttribute("aria-selected", color === this.#defaultColor); | ||||
|       button.addEventListener( | ||||
|         "click", | ||||
|         this.#colorSelect.bind(this, paramType, color) | ||||
|       ); | ||||
|       div.append(button); | ||||
|     } | ||||
| 
 | ||||
|     div.addEventListener("keydown", this.#boundKeyDown); | ||||
| 
 | ||||
|     return div; | ||||
|   } | ||||
| 
 | ||||
|   #colorSelect(type, color, event) { | ||||
|     event.stopPropagation(); | ||||
|     this.#eventBus.dispatch("switchannotationeditorparams", { | ||||
|       source: this, | ||||
|       type, | ||||
|       value: color, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   _colorSelectFromKeyboard(event) { | ||||
|     const color = event.target.getAttribute("data-color"); | ||||
|     if (!color) { | ||||
|       return; | ||||
|     } | ||||
|     this.#colorSelect(color, event); | ||||
|   } | ||||
| 
 | ||||
|   _moveToNext(event) { | ||||
|     if (event.target === this.#button) { | ||||
|       this.#dropdown.firstChild?.focus(); | ||||
|       return; | ||||
|     } | ||||
|     event.target.nextSibling?.focus(); | ||||
|   } | ||||
| 
 | ||||
|   _moveToPrevious(event) { | ||||
|     event.target.previousSibling?.focus(); | ||||
|   } | ||||
| 
 | ||||
|   _moveToBeginning() { | ||||
|     this.#dropdown.firstChild?.focus(); | ||||
|   } | ||||
| 
 | ||||
|   _moveToEnd() { | ||||
|     this.#dropdown.lastChild?.focus(); | ||||
|   } | ||||
| 
 | ||||
|   #keyDown(event) { | ||||
|     ColorPicker._keyboardManager.exec(this, event); | ||||
|   } | ||||
| 
 | ||||
|   #openDropdown(event) { | ||||
|     if (this.#dropdown && !this.#dropdown.classList.contains("hidden")) { | ||||
|       this.hideDropdown(); | ||||
|       return; | ||||
|     } | ||||
|     this.#button.addEventListener("keydown", this.#boundKeyDown); | ||||
|     this.#dropdownWasFromKeyboard = event.detail === 0; | ||||
|     if (this.#dropdown) { | ||||
|       this.#dropdown.classList.remove("hidden"); | ||||
|       return; | ||||
|     } | ||||
|     const root = (this.#dropdown = this.#getDropdownRoot( | ||||
|       AnnotationEditorParamsType.HIGHLIGHT_COLOR | ||||
|     )); | ||||
|     this.#button.append(root); | ||||
|   } | ||||
| 
 | ||||
|   hideDropdown() { | ||||
|     this.#dropdown?.classList.add("hidden"); | ||||
|   } | ||||
| 
 | ||||
|   _hideDropdownFromKeyboard() { | ||||
|     if ( | ||||
|       this.#isMainColorPicker || | ||||
|       !this.#dropdown || | ||||
|       this.#dropdown.classList.contains("hidden") | ||||
|     ) { | ||||
|       return; | ||||
|     } | ||||
|     this.hideDropdown(); | ||||
|     this.#button.removeEventListener("keydown", this.#boundKeyDown); | ||||
|     this.#button.focus({ | ||||
|       preventScroll: true, | ||||
|       focusVisible: this.#dropdownWasFromKeyboard, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   updateColor(color) { | ||||
|     if (this.#buttonSwatch) { | ||||
|       this.#buttonSwatch.style.backgroundColor = color; | ||||
|     } | ||||
|     if (!this.#dropdown) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const i = this.#uiManager.highlightColors.values(); | ||||
|     for (const child of this.#dropdown.children) { | ||||
|       child.setAttribute("aria-selected", i.next().value === color); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   destroy() { | ||||
|     this.#button?.remove(); | ||||
|     this.#button = null; | ||||
|     this.#buttonSwatch = null; | ||||
|     this.#dropdown?.remove(); | ||||
|     this.#dropdown = null; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export { ColorPicker }; | ||||
| @ -903,15 +903,21 @@ class AnnotationEditor { | ||||
|     this.#altText?.finish(); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Add a toolbar for this editor. | ||||
|    * @returns {Promise<EditorToolbar|null>} | ||||
|    */ | ||||
|   async addEditToolbar() { | ||||
|     if (this.#editToolbar || this.#isInEditMode) { | ||||
|       return; | ||||
|       return this.#editToolbar; | ||||
|     } | ||||
|     this.#editToolbar = new EditorToolbar(this); | ||||
|     this.div.append(this.#editToolbar.render()); | ||||
|     if (this.#altText) { | ||||
|       this.#editToolbar.addAltTextButton(await this.#altText.render()); | ||||
|     } | ||||
| 
 | ||||
|     return this.#editToolbar; | ||||
|   } | ||||
| 
 | ||||
|   removeEditToolbar() { | ||||
|  | ||||
| @ -20,6 +20,7 @@ import { | ||||
| } from "../../shared/util.js"; | ||||
| import { AnnotationEditor } from "./editor.js"; | ||||
| import { bindEvents } from "./tools.js"; | ||||
| import { ColorPicker } from "./color_picker.js"; | ||||
| import { Outliner } from "./outliner.js"; | ||||
| 
 | ||||
| /** | ||||
| @ -30,7 +31,7 @@ class HighlightEditor extends AnnotationEditor { | ||||
| 
 | ||||
|   #clipPathId = null; | ||||
| 
 | ||||
|   #color; | ||||
|   #colorPicker = null; | ||||
| 
 | ||||
|   #focusOutlines = null; | ||||
| 
 | ||||
| @ -46,9 +47,9 @@ class HighlightEditor extends AnnotationEditor { | ||||
| 
 | ||||
|   #outlineId = null; | ||||
| 
 | ||||
|   static _defaultColor = "#FFF066"; | ||||
|   static _defaultColor = null; | ||||
| 
 | ||||
|   static _defaultOpacity = 0.4; | ||||
|   static _defaultOpacity = 1; | ||||
| 
 | ||||
|   static _l10nPromise; | ||||
| 
 | ||||
| @ -58,7 +59,9 @@ class HighlightEditor extends AnnotationEditor { | ||||
| 
 | ||||
|   constructor(params) { | ||||
|     super({ ...params, name: "highlightEditor" }); | ||||
|     this.#color = params.color || HighlightEditor._defaultColor; | ||||
|     HighlightEditor._defaultColor ||= | ||||
|       this._uiManager.highlightColors?.values().next().value || "#fff066"; | ||||
|     this.color = params.color || HighlightEditor._defaultColor; | ||||
|     this.#opacity = params.opacity || HighlightEditor._defaultOpacity; | ||||
|     this.#boxes = params.boxes || null; | ||||
|     this._isDraggable = false; | ||||
| @ -100,12 +103,9 @@ class HighlightEditor extends AnnotationEditor { | ||||
| 
 | ||||
|   static updateDefaultParams(type, value) { | ||||
|     switch (type) { | ||||
|       case AnnotationEditorParamsType.HIGHLIGHT_COLOR: | ||||
|       case AnnotationEditorParamsType.HIGHLIGHT_DEFAULT_COLOR: | ||||
|         HighlightEditor._defaultColor = value; | ||||
|         break; | ||||
|       case AnnotationEditorParamsType.HIGHLIGHT_OPACITY: | ||||
|         HighlightEditor._defaultOpacity = value / 100; | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -120,22 +120,15 @@ class HighlightEditor extends AnnotationEditor { | ||||
|       case AnnotationEditorParamsType.HIGHLIGHT_COLOR: | ||||
|         this.#updateColor(value); | ||||
|         break; | ||||
|       case AnnotationEditorParamsType.HIGHLIGHT_OPACITY: | ||||
|         this.#updateOpacity(value); | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   static get defaultPropertiesToUpdate() { | ||||
|     return [ | ||||
|       [ | ||||
|         AnnotationEditorParamsType.HIGHLIGHT_COLOR, | ||||
|         AnnotationEditorParamsType.HIGHLIGHT_DEFAULT_COLOR, | ||||
|         HighlightEditor._defaultColor, | ||||
|       ], | ||||
|       [ | ||||
|         AnnotationEditorParamsType.HIGHLIGHT_OPACITY, | ||||
|         Math.round(HighlightEditor._defaultOpacity * 100), | ||||
|       ], | ||||
|     ]; | ||||
|   } | ||||
| 
 | ||||
| @ -144,11 +137,7 @@ class HighlightEditor extends AnnotationEditor { | ||||
|     return [ | ||||
|       [ | ||||
|         AnnotationEditorParamsType.HIGHLIGHT_COLOR, | ||||
|         this.#color || HighlightEditor._defaultColor, | ||||
|       ], | ||||
|       [ | ||||
|         AnnotationEditorParamsType.HIGHLIGHT_OPACITY, | ||||
|         Math.round(100 * (this.#opacity ?? HighlightEditor._defaultOpacity)), | ||||
|         this.color || HighlightEditor._defaultColor, | ||||
|       ], | ||||
|     ]; | ||||
|   } | ||||
| @ -161,12 +150,14 @@ class HighlightEditor extends AnnotationEditor { | ||||
|     const savedColor = this.color; | ||||
|     this.addCommands({ | ||||
|       cmd: () => { | ||||
|         this.#color = color; | ||||
|         this.color = color; | ||||
|         this.parent.drawLayer.changeColor(this.#id, color); | ||||
|         this.#colorPicker?.updateColor(color); | ||||
|       }, | ||||
|       undo: () => { | ||||
|         this.#color = savedColor; | ||||
|         this.color = savedColor; | ||||
|         this.parent.drawLayer.changeColor(this.#id, savedColor); | ||||
|         this.#colorPicker?.updateColor(savedColor); | ||||
|       }, | ||||
|       mustExec: true, | ||||
|       type: AnnotationEditorParamsType.HIGHLIGHT_COLOR, | ||||
| @ -175,27 +166,17 @@ class HighlightEditor extends AnnotationEditor { | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Update the opacity and make this action undoable. | ||||
|    * @param {number} opacity | ||||
|    */ | ||||
|   #updateOpacity(opacity) { | ||||
|     opacity /= 100; | ||||
|     const savedOpacity = this.#opacity; | ||||
|     this.addCommands({ | ||||
|       cmd: () => { | ||||
|         this.#opacity = opacity; | ||||
|         this.parent.drawLayer.changeOpacity(this.#id, opacity); | ||||
|       }, | ||||
|       undo: () => { | ||||
|         this.#opacity = savedOpacity; | ||||
|         this.parent.drawLayer.changeOpacity(this.#id, savedOpacity); | ||||
|       }, | ||||
|       mustExec: true, | ||||
|       type: AnnotationEditorParamsType.HIGHLIGHT_OPACITY, | ||||
|       overwriteIfSameType: true, | ||||
|       keepUndo: true, | ||||
|     }); | ||||
|   /** @inheritdoc */ | ||||
|   async addEditToolbar() { | ||||
|     const toolbar = await super.addEditToolbar(); | ||||
|     if (!toolbar) { | ||||
|       return null; | ||||
|     } | ||||
|     if (this._uiManager.highlightColors) { | ||||
|       this.#colorPicker = new ColorPicker({ editor: this }); | ||||
|       toolbar.addColorPicker(this.#colorPicker); | ||||
|     } | ||||
|     return toolbar; | ||||
|   } | ||||
| 
 | ||||
|   /** @inheritdoc */ | ||||
| @ -286,7 +267,7 @@ class HighlightEditor extends AnnotationEditor { | ||||
|     ({ id: this.#id, clipPathId: this.#clipPathId } = | ||||
|       parent.drawLayer.highlight( | ||||
|         this.#highlightOutlines, | ||||
|         this.#color, | ||||
|         this.color, | ||||
|         this.#opacity | ||||
|       )); | ||||
|     if (this.#highlightDiv) { | ||||
| @ -424,7 +405,7 @@ class HighlightEditor extends AnnotationEditor { | ||||
|     const editor = super.deserialize(data, parent, uiManager); | ||||
| 
 | ||||
|     const { rect, color, quadPoints } = data; | ||||
|     editor.#color = Util.makeHexColor(...color); | ||||
|     editor.color = Util.makeHexColor(...color); | ||||
|     editor.#opacity = data.opacity; | ||||
| 
 | ||||
|     const [pageWidth, pageHeight] = editor.pageDimensions; | ||||
| @ -452,7 +433,7 @@ class HighlightEditor extends AnnotationEditor { | ||||
|     } | ||||
| 
 | ||||
|     const rect = this.getRect(0, 0); | ||||
|     const color = AnnotationEditor._colorManager.convert(this.#color); | ||||
|     const color = AnnotationEditor._colorManager.convert(this.color); | ||||
| 
 | ||||
|     return { | ||||
|       annotationType: AnnotationEditorType.HIGHLIGHT, | ||||
|  | ||||
| @ -18,6 +18,8 @@ import { noContextMenu } from "../display_utils.js"; | ||||
| class EditorToolbar { | ||||
|   #toolbar = null; | ||||
| 
 | ||||
|   #colorPicker = null; | ||||
| 
 | ||||
|   #editor; | ||||
| 
 | ||||
|   #buttons = null; | ||||
| @ -85,6 +87,7 @@ class EditorToolbar { | ||||
| 
 | ||||
|   hide() { | ||||
|     this.#toolbar.classList.add("hidden"); | ||||
|     this.#colorPicker?.hideDropdown(); | ||||
|   } | ||||
| 
 | ||||
|   show() { | ||||
| @ -106,19 +109,28 @@ class EditorToolbar { | ||||
|     this.#buttons.append(button); | ||||
|   } | ||||
| 
 | ||||
|   addAltTextButton(button) { | ||||
|     this.#addListenersToElement(button); | ||||
|     this.#buttons.prepend(button, this.#divider); | ||||
|   } | ||||
| 
 | ||||
|   get #divider() { | ||||
|     const divider = document.createElement("div"); | ||||
|     divider.className = "divider"; | ||||
|     return divider; | ||||
|   } | ||||
| 
 | ||||
|   addAltTextButton(button) { | ||||
|     this.#addListenersToElement(button); | ||||
|     this.#buttons.prepend(button, this.#divider); | ||||
|   } | ||||
| 
 | ||||
|   addColorPicker(colorPicker) { | ||||
|     this.#colorPicker = colorPicker; | ||||
|     const button = colorPicker.renderButton(); | ||||
|     this.#addListenersToElement(button); | ||||
|     this.#buttons.prepend(button, this.#divider); | ||||
|   } | ||||
| 
 | ||||
|   remove() { | ||||
|     this.#toolbar.remove(); | ||||
|     this.#colorPicker?.destroy(); | ||||
|     this.#colorPicker = null; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -438,7 +438,7 @@ class KeyboardManager { | ||||
|     if (checker && !checker(self, event)) { | ||||
|       return; | ||||
|     } | ||||
|     callback.bind(self, ...args)(); | ||||
|     callback.bind(self, ...args, event)(); | ||||
| 
 | ||||
|     // For example, ctrl+s in a FreeText must be handled by the viewer, hence
 | ||||
|     // the event must bubble.
 | ||||
| @ -545,6 +545,8 @@ class AnnotationEditorUIManager { | ||||
| 
 | ||||
|   #focusMainContainerTimeoutId = null; | ||||
| 
 | ||||
|   #highlightColors = null; | ||||
| 
 | ||||
|   #idManager = new IdManager(); | ||||
| 
 | ||||
|   #isEnabled = false; | ||||
| @ -553,6 +555,8 @@ class AnnotationEditorUIManager { | ||||
| 
 | ||||
|   #lastActiveElement = null; | ||||
| 
 | ||||
|   #mainHighlightColorPicker = null; | ||||
| 
 | ||||
|   #mode = AnnotationEditorType.NONE; | ||||
| 
 | ||||
|   #selectedEditors = new Set(); | ||||
| @ -607,6 +611,7 @@ class AnnotationEditorUIManager { | ||||
|       // For example, sliders can be controlled with the arrow keys.
 | ||||
|       return ( | ||||
|         self.#container.contains(document.activeElement) && | ||||
|         document.activeElement.tagName !== "BUTTON" && | ||||
|         self.hasSomethingToControl() | ||||
|       ); | ||||
|     }; | ||||
| @ -736,7 +741,8 @@ class AnnotationEditorUIManager { | ||||
|     altTextManager, | ||||
|     eventBus, | ||||
|     pdfDocument, | ||||
|     pageColors | ||||
|     pageColors, | ||||
|     highlightColors | ||||
|   ) { | ||||
|     this.#container = container; | ||||
|     this.#viewer = viewer; | ||||
| @ -749,6 +755,7 @@ class AnnotationEditorUIManager { | ||||
|     this.#annotationStorage = pdfDocument.annotationStorage; | ||||
|     this.#filterFactory = pdfDocument.filterFactory; | ||||
|     this.#pageColors = pageColors; | ||||
|     this.#highlightColors = highlightColors || null; | ||||
|     this.viewParameters = { | ||||
|       realScale: PixelsPerInch.PDF_TO_CSS_UNITS, | ||||
|       rotation: 0, | ||||
| @ -803,6 +810,24 @@ class AnnotationEditorUIManager { | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   get highlightColors() { | ||||
|     return shadow( | ||||
|       this, | ||||
|       "highlightColors", | ||||
|       this.#highlightColors | ||||
|         ? new Map( | ||||
|             this.#highlightColors | ||||
|               .split(",") | ||||
|               .map(pair => pair.split("=").map(x => x.trim())) | ||||
|           ) | ||||
|         : null | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   setMainHighlightColorPicker(colorPicker) { | ||||
|     this.#mainHighlightColorPicker = colorPicker; | ||||
|   } | ||||
| 
 | ||||
|   editAltText(editor) { | ||||
|     this.#altTextManager?.editAltText(this, editor); | ||||
|   } | ||||
| @ -1246,9 +1271,14 @@ class AnnotationEditorUIManager { | ||||
|     if (!this.#editorTypes) { | ||||
|       return; | ||||
|     } | ||||
|     if (type === AnnotationEditorParamsType.CREATE) { | ||||
| 
 | ||||
|     switch (type) { | ||||
|       case AnnotationEditorParamsType.CREATE: | ||||
|         this.currentLayer.addNewEditor(); | ||||
|         return; | ||||
|       case AnnotationEditorParamsType.HIGHLIGHT_DEFAULT_COLOR: | ||||
|         this.#mainHighlightColorPicker?.updateColor(value); | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     for (const editor of this.#selectedEditors) { | ||||
|  | ||||
| @ -70,6 +70,7 @@ import { renderTextLayer, updateTextLayer } from "./display/text_layer.js"; | ||||
| import { AnnotationEditorLayer } from "./display/editor/annotation_editor_layer.js"; | ||||
| import { AnnotationEditorUIManager } from "./display/editor/tools.js"; | ||||
| import { AnnotationLayer } from "./display/annotation_layer.js"; | ||||
| import { ColorPicker } from "./display/editor/color_picker.js"; | ||||
| import { DrawLayer } from "./display/draw_layer.js"; | ||||
| import { GlobalWorkerOptions } from "./display/worker_options.js"; | ||||
| import { Outliner } from "./display/editor/outliner.js"; | ||||
| @ -92,6 +93,7 @@ export { | ||||
|   AnnotationMode, | ||||
|   build, | ||||
|   CMapCompressionType, | ||||
|   ColorPicker, | ||||
|   createValidAbsoluteUrl, | ||||
|   DOMSVGFactory, | ||||
|   DrawLayer, | ||||
|  | ||||
| @ -87,7 +87,7 @@ const AnnotationEditorParamsType = { | ||||
|   INK_THICKNESS: 22, | ||||
|   INK_OPACITY: 23, | ||||
|   HIGHLIGHT_COLOR: 31, | ||||
|   HIGHLIGHT_OPACITY: 32, | ||||
|   HIGHLIGHT_DEFAULT_COLOR: 32, | ||||
| }; | ||||
| 
 | ||||
| // Permission flags from Table 22, Section 7.6.3.2 of the PDF specification.
 | ||||
|  | ||||
| @ -63,6 +63,7 @@ import { | ||||
| import { AnnotationEditorLayer } from "../../src/display/editor/annotation_editor_layer.js"; | ||||
| import { AnnotationEditorUIManager } from "../../src/display/editor/tools.js"; | ||||
| import { AnnotationLayer } from "../../src/display/annotation_layer.js"; | ||||
| import { ColorPicker } from "../../src/display/editor/color_picker.js"; | ||||
| import { DrawLayer } from "../../src/display/draw_layer.js"; | ||||
| import { GlobalWorkerOptions } from "../../src/display/worker_options.js"; | ||||
| import { Outliner } from "../../src/display/editor/outliner.js"; | ||||
| @ -78,6 +79,7 @@ const expectedAPI = Object.freeze({ | ||||
|   AnnotationMode, | ||||
|   build, | ||||
|   CMapCompressionType, | ||||
|   ColorPicker, | ||||
|   createValidAbsoluteUrl, | ||||
|   DOMSVGFactory, | ||||
|   DrawLayer, | ||||
|  | ||||
| @ -904,6 +904,38 @@ | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .colorPicker { | ||||
|   --hover-outline-color: #0250bb; | ||||
|   --selected-outline-color: #0060df; | ||||
|   --swatch-border-color: #cfcfd8; | ||||
| 
 | ||||
|   @media (prefers-color-scheme: dark) { | ||||
|     --hover-outline-color: #80ebff; | ||||
|     --selected-outline-color: #aaf2ff; | ||||
|     --swatch-border-color: #52525e; | ||||
|   } | ||||
| 
 | ||||
|   @media screen and (forced-colors: active) { | ||||
|     --hover-outline-color: Highlight; | ||||
|     --selected-outline-color: var(--hover-outline-color); | ||||
|     --swatch-border-color: ButtonText; | ||||
|   } | ||||
| 
 | ||||
|   .swatch { | ||||
|     width: 16px; | ||||
|     height: 16px; | ||||
|     border: 1px solid var(--swatch-border-color); | ||||
|     border-radius: 100%; | ||||
|     outline-offset: 2px; | ||||
|     box-sizing: border-box; | ||||
|     forced-color-adjust: none; | ||||
|   } | ||||
| 
 | ||||
|   button:is(:hover, .selected) > .swatch { | ||||
|     border: none; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .annotationEditorLayer { | ||||
|   &[data-main-rotation="0"] { | ||||
|     .highlightEditor > .editToolbar { | ||||
| @ -962,7 +994,144 @@ | ||||
|     } | ||||
| 
 | ||||
|     .editToolbar { | ||||
|       --editor-toolbar-colorpicker-arrow-image: url(images/toolbarButton-menuArrow.svg); | ||||
| 
 | ||||
|       transform-origin: center !important; | ||||
| 
 | ||||
|       .buttons { | ||||
|         .colorPicker { | ||||
|           position: relative; | ||||
|           width: auto; | ||||
|           display: flex; | ||||
|           justify-content: center; | ||||
|           align-items: center; | ||||
|           gap: 4px; | ||||
|           padding: 4px; | ||||
| 
 | ||||
|           &::after { | ||||
|             content: ""; | ||||
|             mask-image: var(--editor-toolbar-colorpicker-arrow-image); | ||||
|             mask-repeat: no-repeat; | ||||
|             mask-position: center; | ||||
|             display: inline-block; | ||||
|             background-color: var(--editor-toolbar-fg-color); | ||||
|             width: 12px; | ||||
|             height: 12px; | ||||
|           } | ||||
| 
 | ||||
|           &:hover::after { | ||||
|             background-color: var(--editor-toolbar-hover-fg-color); | ||||
|           } | ||||
| 
 | ||||
|           &:has(.dropdown:not(.hidden)) { | ||||
|             background-color: var(--editor-toolbar-hover-bg-color); | ||||
|           } | ||||
| 
 | ||||
|           .dropdown { | ||||
|             position: absolute; | ||||
|             display: flex; | ||||
|             justify-content: center; | ||||
|             align-items: center; | ||||
|             flex-direction: column; | ||||
|             gap: 11px; | ||||
|             padding-block: 8px; | ||||
|             border-radius: 6px; | ||||
|             background-color: var(--editor-toolbar-bg-color); | ||||
|             border: 1px solid var(--editor-toolbar-border-color); | ||||
|             box-shadow: var(--editor-toolbar-shadow); | ||||
|             inset-block-start: calc(100% + 4px); | ||||
|             width: calc(100% + 2 * var(--editor-toolbar-padding)); | ||||
| 
 | ||||
|             button { | ||||
|               width: 100%; | ||||
|               height: auto; | ||||
|               border: none; | ||||
|               cursor: pointer; | ||||
|               display: flex; | ||||
|               justify-content: center; | ||||
|               align-items: center; | ||||
|               background: none; | ||||
| 
 | ||||
|               &:is(:active, :focus-visible) { | ||||
|                 outline: none; | ||||
|               } | ||||
| 
 | ||||
|               > .swatch { | ||||
|                 outline-offset: 2px; | ||||
|               } | ||||
| 
 | ||||
|               &[aria-selected="true"] > .swatch { | ||||
|                 outline: 2px solid var(--selected-outline-color); | ||||
|               } | ||||
| 
 | ||||
|               &:is(:hover, :active, :focus-visible) > .swatch { | ||||
|                 outline: 2px solid var(--hover-outline-color); | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .editorParamsToolbar:has(#highlightParamsToolbarContainer) { | ||||
|   padding: unset; | ||||
| } | ||||
| 
 | ||||
| #highlightParamsToolbarContainer { | ||||
|   height: auto; | ||||
|   padding-inline: 10px; | ||||
|   padding-block: 10px 16px; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   box-sizing: border-box; | ||||
| 
 | ||||
|   .colorPicker { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     gap: 8px; | ||||
| 
 | ||||
|     #highlightColorPickerLabel { | ||||
|       width: fit-content; | ||||
|       inset-inline-start: 0; | ||||
|     } | ||||
| 
 | ||||
|     .dropdown { | ||||
|       display: flex; | ||||
|       justify-content: space-between; | ||||
|       align-items: center; | ||||
|       flex-direction: row; | ||||
|       height: auto; | ||||
| 
 | ||||
|       button { | ||||
|         width: auto; | ||||
|         height: auto; | ||||
|         border: none; | ||||
|         cursor: pointer; | ||||
|         display: flex; | ||||
|         justify-content: center; | ||||
|         align-items: center; | ||||
|         background: none; | ||||
|         flex: 0 0 auto; | ||||
| 
 | ||||
|         .swatch { | ||||
|           width: 24px; | ||||
|           height: 24px; | ||||
|         } | ||||
| 
 | ||||
|         &:is(:active, :focus-visible) { | ||||
|           outline: none; | ||||
|         } | ||||
| 
 | ||||
|         &[aria-selected="true"] > .swatch { | ||||
|           outline: 2px solid var(--selected-outline-color); | ||||
|         } | ||||
| 
 | ||||
|         &:is(:hover, :active, :focus-visible) > .swatch { | ||||
|           outline: 2px solid var(--hover-outline-color); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -28,8 +28,6 @@ class AnnotationEditorParams { | ||||
|   #bindListeners({ | ||||
|     editorFreeTextFontSize, | ||||
|     editorFreeTextColor, | ||||
|     editorHighlightColor, | ||||
|     editorHighlightOpacity, | ||||
|     editorInkColor, | ||||
|     editorInkThickness, | ||||
|     editorInkOpacity, | ||||
| @ -48,12 +46,6 @@ class AnnotationEditorParams { | ||||
|     editorFreeTextColor.addEventListener("input", function () { | ||||
|       dispatchEvent("FREETEXT_COLOR", this.value); | ||||
|     }); | ||||
|     editorHighlightColor.addEventListener("input", function () { | ||||
|       dispatchEvent("HIGHLIGHT_COLOR", this.value); | ||||
|     }); | ||||
|     editorHighlightOpacity.addEventListener("input", function () { | ||||
|       dispatchEvent("HIGHLIGHT_OPACITY", this.valueAsNumber); | ||||
|     }); | ||||
|     editorInkColor.addEventListener("input", function () { | ||||
|       dispatchEvent("INK_COLOR", this.value); | ||||
|     }); | ||||
| @ -76,12 +68,6 @@ class AnnotationEditorParams { | ||||
|           case AnnotationEditorParamsType.FREETEXT_COLOR: | ||||
|             editorFreeTextColor.value = value; | ||||
|             break; | ||||
|           case AnnotationEditorParamsType.HIGHLIGHT_COLOR: | ||||
|             editorHighlightColor.value = value; | ||||
|             break; | ||||
|           case AnnotationEditorParamsType.HIGHLIGHT_OPACITY: | ||||
|             editorHighlightOpacity.value = value; | ||||
|             break; | ||||
|           case AnnotationEditorParamsType.INK_COLOR: | ||||
|             editorInkColor.value = value; | ||||
|             break; | ||||
|  | ||||
| @ -442,6 +442,7 @@ const PDFViewerApplication = { | ||||
|       textLayerMode: AppOptions.get("textLayerMode"), | ||||
|       annotationMode: AppOptions.get("annotationMode"), | ||||
|       annotationEditorMode, | ||||
|       annotationEditorHighlightColors: AppOptions.get("highlightEditorColors"), | ||||
|       imageResourcesPath: AppOptions.get("imageResourcesPath"), | ||||
|       enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"), | ||||
|       isOffscreenCanvasSupported, | ||||
|  | ||||
| @ -158,6 +158,11 @@ const defaultOptions = { | ||||
|     value: 0, | ||||
|     kind: OptionKind.VIEWER + OptionKind.PREFERENCE, | ||||
|   }, | ||||
|   highlightEditorColors: { | ||||
|     /** @type {string} */ | ||||
|     value: "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F", | ||||
|     kind: OptionKind.VIEWER + OptionKind.PREFERENCE, | ||||
|   }, | ||||
|   historyUpdateUrl: { | ||||
|     /** @type {boolean} */ | ||||
|     value: false, | ||||
|  | ||||
| @ -36,8 +36,14 @@ | ||||
|     } | ||||
| 
 | ||||
|     &.highlight { | ||||
|       --blend-mode: multiply; | ||||
| 
 | ||||
|       @media screen and (forced-colors: active) { | ||||
|         --blend-mode: difference; | ||||
|       } | ||||
| 
 | ||||
|       position: absolute; | ||||
|       mix-blend-mode: multiply; | ||||
|       mix-blend-mode: var(--blend-mode); | ||||
|       fill-rule: evenodd; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -109,6 +109,8 @@ function isValidAnnotationEditorMode(mode) { | ||||
|  * @property {number} [annotationEditorMode] - Enables the creation and editing | ||||
|  *   of new Annotations. The constants from {@link AnnotationEditorType} should | ||||
|  *   be used. The default value is `AnnotationEditorType.NONE`. | ||||
|  * @property {string} [annotationEditorHighlightColors] - A comma separated list | ||||
|  *   of colors to propose to highlight some text in the pdf. | ||||
|  * @property {string} [imageResourcesPath] - Path for image resources, mainly | ||||
|  *   mainly for annotation icons. Include trailing slash. | ||||
|  * @property {boolean} [enablePrintAutoRotate] - Enables automatic rotation of | ||||
| @ -202,6 +204,8 @@ class PDFViewer { | ||||
| 
 | ||||
|   #altTextManager = null; | ||||
| 
 | ||||
|   #annotationEditorHighlightColors = null; | ||||
| 
 | ||||
|   #annotationEditorMode = AnnotationEditorType.NONE; | ||||
| 
 | ||||
|   #annotationEditorUIManager = null; | ||||
| @ -276,6 +280,8 @@ class PDFViewer { | ||||
|       options.annotationMode ?? AnnotationMode.ENABLE_FORMS; | ||||
|     this.#annotationEditorMode = | ||||
|       options.annotationEditorMode ?? AnnotationEditorType.NONE; | ||||
|     this.#annotationEditorHighlightColors = | ||||
|       options.annotationEditorHighlightColors || null; | ||||
|     this.imageResourcesPath = options.imageResourcesPath || ""; | ||||
|     this.enablePrintAutoRotate = options.enablePrintAutoRotate || false; | ||||
|     if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { | ||||
| @ -862,8 +868,13 @@ class PDFViewer { | ||||
|               this.#altTextManager, | ||||
|               this.eventBus, | ||||
|               pdfDocument, | ||||
|               this.pageColors | ||||
|               this.pageColors, | ||||
|               this.#annotationEditorHighlightColors | ||||
|             ); | ||||
|             this.eventBus.dispatch("annotationeditoruimanager", { | ||||
|               source: this, | ||||
|               uiManager: this.#annotationEditorUIManager, | ||||
|             }); | ||||
|             if (mode !== AnnotationEditorType.NONE) { | ||||
|               this.#annotationEditorUIManager.updateMode(mode); | ||||
|             } | ||||
|  | ||||
| @ -32,6 +32,7 @@ const { | ||||
|   AnnotationMode, | ||||
|   build, | ||||
|   CMapCompressionType, | ||||
|   ColorPicker, | ||||
|   createValidAbsoluteUrl, | ||||
|   DOMSVGFactory, | ||||
|   DrawLayer, | ||||
| @ -80,6 +81,7 @@ export { | ||||
|   AnnotationMode, | ||||
|   build, | ||||
|   CMapCompressionType, | ||||
|   ColorPicker, | ||||
|   createValidAbsoluteUrl, | ||||
|   DOMSVGFactory, | ||||
|   DrawLayer, | ||||
|  | ||||
| @ -13,7 +13,7 @@ | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| 
 | ||||
| import { AnnotationEditorType, noContextMenu } from "pdfjs-lib"; | ||||
| import { AnnotationEditorType, ColorPicker, noContextMenu } from "pdfjs-lib"; | ||||
| import { | ||||
|   DEFAULT_SCALE, | ||||
|   DEFAULT_SCALE_VALUE, | ||||
| @ -120,9 +120,24 @@ class Toolbar { | ||||
|     // Bind the event listeners for click and various other actions.
 | ||||
|     this.#bindListeners(options); | ||||
| 
 | ||||
|     if (options.editorHighlightColorPicker) { | ||||
|       this.eventBus._on("annotationeditoruimanager", ({ uiManager }) => { | ||||
|         this.#setAnnotationEditorUIManager( | ||||
|           uiManager, | ||||
|           options.editorHighlightColorPicker | ||||
|         ); | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     this.reset(); | ||||
|   } | ||||
| 
 | ||||
|   #setAnnotationEditorUIManager(uiManager, parentContainer) { | ||||
|     const colorPicker = new ColorPicker({ uiManager }); | ||||
|     uiManager.setMainHighlightColorPicker(colorPicker); | ||||
|     parentContainer.append(colorPicker.renderMainDropdown()); | ||||
|   } | ||||
| 
 | ||||
|   setPageNumber(pageNumber, pageLabel) { | ||||
|     this.pageNumber = pageNumber; | ||||
|     this.pageLabel = pageLabel; | ||||
|  | ||||
| @ -532,6 +532,11 @@ body { | ||||
| .editorParamsToolbarContainer .editorParamsLabel { | ||||
|   padding-inline-end: 10px; | ||||
|   flex: none; | ||||
|   font: menu; | ||||
|   font-size: 13px; | ||||
|   font-style: normal; | ||||
|   font-weight: 400; | ||||
|   line-height: 150%; | ||||
|   color: var(--main-color); | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -172,14 +172,9 @@ See https://github.com/adobe-type-tools/cmap-resources | ||||
|         </div>  <!-- findbar --> | ||||
| 
 | ||||
|         <div class="editorParamsToolbar hidden doorHangerRight" id="editorHighlightParamsToolbar"> | ||||
|           <div class="editorParamsToolbarContainer"> | ||||
|             <div class="editorParamsSetter"> | ||||
|               <label for="editorHighlightColor" class="editorParamsLabel" data-l10n-id="editor_highlight_color">Color</label> | ||||
|               <input type="color" value="#FFFF00" id="editorHighlightColor" class="editorParamsColor" tabindex="100"> | ||||
|             </div> | ||||
|             <div class="editorParamsSetter"> | ||||
|               <label for="editorHighlightOpacity" class="editorParamsLabel" data-l10n-id="editor_highlight_opacity">Opacity</label> | ||||
|               <input type="range" id="editorHighlightOpacity" class="editorParamsSlider" value="100" min="1" max="100" step="1" tabindex="101"> | ||||
|           <div id="highlightParamsToolbarContainer" class="editorParamsToolbarContainer"> | ||||
|             <div id="editorHighlightColorPicker" class="colorPicker"> | ||||
|               <span id="highlightColorPickerLabel" class="editorParamsLabel" data-l10n-id="pdfjs-editor-highlight-colorpicker-label">Highlight color</span> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
| @ -217,7 +212,7 @@ See https://github.com/adobe-type-tools/cmap-resources | ||||
|         <div class="editorParamsToolbar hidden doorHangerRight" id="editorStampParamsToolbar"> | ||||
|           <div class="editorParamsToolbarContainer"> | ||||
|             <button id="editorStampAddImage" class="secondaryToolbarButton" title="Add image" tabindex="107" data-l10n-id="pdfjs-editor-stamp-add-image-button"> | ||||
|               <span data-l10n-id="pdfjs-editor-stamp-add-image-button-label">Add image</span> | ||||
|               <span class="editorParamsLabel" data-l10n-id="pdfjs-editor-stamp-add-image-button-label">Add image</span> | ||||
|             </button> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
| @ -61,6 +61,9 @@ function getViewerConfiguration() { | ||||
|       editorHighlightParamsToolbar: document.getElementById( | ||||
|         "editorHighlightParamsToolbar" | ||||
|       ), | ||||
|       editorHighlightColorPicker: document.getElementById( | ||||
|         "editorHighlightColorPicker" | ||||
|       ), | ||||
|       editorInkButton: document.getElementById("editorInk"), | ||||
|       editorInkParamsToolbar: document.getElementById("editorInkParamsToolbar"), | ||||
|       editorStampButton: document.getElementById("editorStamp"), | ||||
| @ -168,8 +171,6 @@ function getViewerConfiguration() { | ||||
|     annotationEditorParams: { | ||||
|       editorFreeTextFontSize: document.getElementById("editorFreeTextFontSize"), | ||||
|       editorFreeTextColor: document.getElementById("editorFreeTextColor"), | ||||
|       editorHighlightColor: document.getElementById("editorHighlightColor"), | ||||
|       editorHighlightOpacity: document.getElementById("editorHighlightOpacity"), | ||||
|       editorInkColor: document.getElementById("editorInkColor"), | ||||
|       editorInkThickness: document.getElementById("editorInkThickness"), | ||||
|       editorInkOpacity: document.getElementById("editorInkOpacity"), | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user