1import { Controller } from "@hotwired/stimulus" 2 3export default class extends Controller { 4 5 static get targets() { 6 return [ 7 'menuRow', 8 'formatOptionList', 9 'h1', 10 'h2', 11 'h3', 12 'bold', 13 'italic', 14 'strike', 15 'bulletList', 16 'orderedList', 17 'quote', 18 'link', 19 'more' 20 // 'palette', 21 // 'paletteItem', 22 // 'image', 23 // 'inputImage', 24 // 'inputFile' 25 ] 26 } 27 28 static get values() { 29 return { 30 colors: String 31 } 32 } 33 34 initialize() { 35 this.colors = this.colorsValue.split(' ') 36 } 37 38 connect() { 39 this.showingMoreMenu = false; 40 this.renderMenuRows(); 41 // this.hideMenuItemArea = (e) => { 42 // this._hideMenuItemArea(e) 43 // } 44 // document.addEventListener("click", this.hideMenuItemArea); 45 } 46 47 disconnect() { 48 // document.removeEventListener("click", this.hideMenuItemArea); 49 } 50 51 /** 52 * Renders the menu rows by adjusting the visibility of menu items based on available space. 53 * It hides or shows menu items and updates the "More" menu as necessary. 54 */ 55 renderMenuRows() { 56 const menuRows = this.menuRowTargets; 57 const rowWidth = this.element.offsetWidth; 58 59 // Number of format options that can be displayed 60 const firstRowCapacity = Math.floor((rowWidth - 16 - 172 - 9 + 4) / 44); 61 const otherRowCapacity = Math.floor((rowWidth - 16 + 4) / 44); 62 63 const allFormatOptions = Array.from(this.element.querySelectorAll('.format-option')); 64 const totalOptions = allFormatOptions.length; 65 66 if (totalOptions <= firstRowCapacity) { 67 // Show all format options in the first row 68 allFormatOptions.forEach((option) => { 69 this.formatOptionListTargets[0].insertBefore(option, this.moreTarget); 70 }); 71 } else { 72 // Show (capacity - 1) format options in the first row 73 const firstRowOptions = allFormatOptions.slice(0, firstRowCapacity - 1); 74 firstRowOptions.forEach((option) => { 75 this.formatOptionListTargets[0].insertBefore(option, this.moreTarget); 76 }); 77 for (let i = 1; i < menuRows.length; i++) { 78 const options = allFormatOptions.slice(firstRowCapacity - 1 + (i - 1) * otherRowCapacity, firstRowCapacity - 1 + i * otherRowCapacity); 79 if (options.length === 0) { 80 break; 81 } 82 options.forEach((option) => { 83 this.formatOptionListTargets[i].appendChild(option); 84 }); 85 } 86 } 87 this._renderMoreMenu(); 88 } 89 90 _renderMoreMenu() { 91 for (let i = 1; i < 3; i++) { 92 if (this.showingMoreMenu && this.formatOptionListTargets[i].children.length > 0) { 93 this.menuRowTargets[i].classList.remove('hidden'); 94 this.menuRowTargets[i].classList.add('flex'); 95 } else { 96 this.menuRowTargets[i].classList.add('hidden'); 97 this.menuRowTargets[i].classList.remove('flex'); 98 } 99 } 100 if (this.formatOptionListTargets[1].children.length === 0) { 101 this.moreTarget.classList.add('hidden'); 102 } else { 103 this.moreTarget.classList.remove('hidden'); 104 } 105 } 106 107 changeTemplate(e) { 108 document.getElementById('modal').insertAdjacentHTML( 109 "beforeend", 110 e.params.modal 111 ); 112 } 113 114 editorConnected(editorController) { 115 // Always re-read the editor: a server-side state replacement rebuilds the 116 // editor instance, and a cached reference would target a destroyed editor. 117 this.editorController = editorController; 118 this.editor = editorController?.editor; 119 this._render(); 120 } 121 122 editorUpdated() { 123 this._render(); 124 } 125 126 h1() { 127 this._editor().chain().focus().toggleHeading({ level: 1 }).run() 128 } 129 130 h2() { 131 this._editor().chain().focus().toggleHeading({ level: 2 }).run() 132 } 133 134 h3() { 135 this._editor().chain().focus().toggleHeading({ level: 3 }).run() 136 } 137 138 bold() { 139 this._editor().chain().focus().toggleBold().run() 140 } 141 142 italic() { 143 this._editor().chain().focus().toggleItalic().run() 144 } 145 146 strike() { 147 this._editor().chain().focus().toggleStrike().run() 148 } 149 150 bulletList() { 151 this._editor().chain().focus().toggleBulletList().run() 152 } 153 154 orderedList() { 155 this._editor().chain().focus().toggleOrderedList().run() 156 } 157 158 quote() { 159 this._editor().chain().focus().toggleBlockquote().run() 160 } 161 162 palette(e) { 163 if (this._isTargetIcon(e, this.paletteTarget)) { 164 this._toggleActiveClass(this.paletteTarget, !this.paletteTarget.classList.contains('active')); 165 this._editor().chain().focus().run() 166 } 167 } 168 169 pickColor({ params }) { 170 this._editor().chain().focus().setColor(params.color).run() 171 } 172 173 more() { 174 this.showingMoreMenu = !this.showingMoreMenu; 175 this._render(); 176 this.renderMenuRows() 177 } 178 179 image() { } 180
181 async imageSelected(e) { 182 const file = e.target.files[0]; 183 if (file) { 184 const { url, error } = await this._editorController().uploadImage(file); 185 if (error) { 186 alert(error); 187 } else { 188 this._editor().chain().focus().setImage({ src: url }).run(); 189 } 190 this.inputImageTarget.value = ""; 191 } 192 } 193 194 file() { } 195 196 async fileSelected(e) { 197 const file = e.target.files[0]; 198 if (file) { 199 const fileName = file.name; 200 const { url, error } = await this._editorController().uploadFile(file); 201 if (error) { 202 alert(error); 203 } else { 204 this._editor().chain().focus().setFile({ href: url, 'data-name': fileName }).run(); 205 } 206 this.inputFileTarget.value = ""; 207 } 208 } 209 210 _editorController() { 211 if (this.editorController) { 212 return this.editorController; 213 } 214 const editorControllerId = "app--editor--component" 215 const editor = this.element.closest(`[data-controller="${editorControllerId}"]`) 216 this.editorController = this.application.getControllerForElementAndIdentifier(editor, editorControllerId) 217 return this.editorController; 218 } 219 220 _editor() { 221 if (this.editor) { 222 return this.editor; 223 } 224 225 this.editor = this._editorController()?.editor 226 return this.editor 227 } 228 229 // _hideMenuItemArea(e) { 230 // [ 231 // // this.headingTarget, 232 // this.paletteTarget 233 // ].forEach((target) => { 234 // if (!target.contains(e.target)) { 235 // target.classList.remove('active'); 236 // } 237 // }) 238 // } 239 240 _render() { 241 const editor = this._editor(); 242 if (!editor) return; 243 244 this._toggleActiveClass(this.h1Target, editor.isActive('heading', { level: 1 })); 245 this._toggleActiveClass(this.h2Target, editor.isActive('heading', { level: 2 })); 246 this._toggleActiveClass(this.h3Target, editor.isActive('heading', { level: 3 })); 247 this._toggleActiveClass(this.boldTarget, editor.isActive('bold')); 248 this._toggleActiveClass(this.italicTarget, editor.isActive('italic')); 249 this._toggleActiveClass(this.strikeTarget, editor.isActive('strike')); 250 this._toggleActiveClass(this.moreTarget, this.showingMoreMenu); 251 // this._renderColorPicker() 252 // this._renderHeading() 253 } 254 255 // _renderColorPicker() { 256 // this.paletteItemTargets.forEach((item) => { 257 // const color = item.getAttribute(`data-${this.identifier}-color-param`) 258 // if (this._editor().isActive('textStyle', { color: color })) { 259 // item.classList.add('active') 260 // } else { 261 // item.classList.remove('active') 262 // } 263 // }) 264 // } 265 266 // _renderHeading() { 267 // this.headingTarget.value = 0; 268 // [1, 2].forEach((level) => { 269 // if (this._editor().isActive('heading', { level: level })) { 270 // this.headingTarget.value = level; 271 // } 272 // }); 273 // } 274 275 _toggleActiveClass(element, isActive) { 276 if (isActive) { 277 element.classList.add('active'); 278 } else { 279 element.classList.remove('active'); 280 } 281 } 282 283 _isTargetIcon(e, target) { 284 return e.target === target || 285 e.target === target.children[0] || 286 target.children[0].contains(e.target) 287 } 288}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.