1import { LitElement, html, css } from 'lit'; 2 3export class RichtextDialog extends LitElement { 4 static properties = { 5 open: { type: Boolean, reflect: true }, 6 value: { type: Object } 7 }; 8 9 // noinspection CssUnusedSymbol 10 static styles = css` 11:host { 12 display: inline-block; 13 position: relative; 14 cursor: pointer; 15} 16.btn { 17 width: 1.5rem; 18 height: 1rem; 19 display: flex; 20 align-items: center; 21 justify-items: center; 22 padding: .25rem; 23 border: 1px solid transparent; 24 border-radius: .25rem; 25 background: transparent; 26} 27.btn:after { 28 content: 'âº'; 29 transform: rotate(90deg); 30 margin-left: .25rem; 31} 32.btn:hover { 33 border: 1px solid #d0d0d0; 34} 35.btn:active, :host([active]) .btn { 36 border: 1px solid #d0d0d0; 37 background-color: #d0d0d0; 38} 39 40.dialog { 41 position: absolute; 42 top: 1.75rem; 43 left: 0; 44 background: #fff; 45 border: 1px solid #d0d0d0; 46 border-radius: .25rem; 47 padding: .5rem; 48 z-index: 10000; 49 display: none; 50 min-width: 200px; 51 box-shadow: 0 4px 8px rgba(0,0,0,0.08); 52} 53.dialog.open { 54 display: block; 55} 56.row { 57 display: flex; 58 gap: .5rem; 59 align-items: center; 60} 61input[type="text"] { 62 flex: 1; 63 padding: .25rem .4rem; 64 border: 1px solid #ddd; 65 border-radius: .25rem; 66} 67button { 68 font-size: .6875rem; 69} 70button.ok { 71 padding: .25rem .6rem; 72 border-radius: .25rem; 73 border: 1px solid #d0d0d0; 74 background: #fff; 75 cursor: pointer; 76} 77button.ok:hover { 78 background: #e9e9e9; 79} 80.actions { 81 margin-top: .5rem; 82 display:flex; 83 justify-content:flex-end; 84 gap:.5rem; 85} 86button.cancel { 87 padding:.2rem .5rem; 88 border-radius:.25rem; 89 border:1px solid transparent; 90 background:transparent; 91 cursor:pointer; 92} 93`; 94 95 constructor() { 96 super(); 97 this.open = false; 98 this.value = {}; 99 this._onDocumentClick = this._onDocumentClick.bind(this); 100 } 101 102 render() { 103 const open = this.open ? 'open' : ''; 104 return html` 105<a class="btn" @mousedown="${this._onMouseDown}" @click="${this._toggle}"> 106 <slot></slot> 107</a> 108<div class="dialog ${open}" role="dialog" @mousedown="${this._onMouseDown}" @click="${this._intercept}" aria-hidden="${!this.open}"> 109 <div class="row"> 110 <slot name="fields"></slot> 111 </div> 112 <div class="actions"> 113 <button class="cancel" @click="${this._onCancel}" type="button">Cancel</button> 114 <button class="ok" @click="${this._onOk}" type="button">OK</button> 115 </div> 116</div> 117`; 118 } 119 120 _intercept(evt) { 121 evt.stopPropagation(); 122 evt.preventDefault(); 123 } 124 125 firstUpdated() { 126 // Ensure clicks outside close the dialog 127 document.addEventListener('click', this._onDocumentClick); 128 } 129 130 disconnectedCallback() { 131 super.disconnectedCallback(); 132 document.removeEventListener('click', this._onDocumentClick); 133 } 134 135 _onMouseDown(evt) { 136 // prevent editor losing focus when used inside a toolbar 137 if (evt.target.nodeName !== 'INPUT') { 138 evt.preventDefault(); 139 } 140 } 141 142 _toggle(evt) { 143 evt.preventDefault(); 144 evt.stopPropagation(); 145 this.open = !this.open; 146 if (this.open) { 147 this._saveSelection(); 148 // focus input next tick 149 requestAnimationFrame(() => { 150 const inp = this.renderRoot.querySelector('#href'); 151 inp?.focus(); 152 inp?.select(); 153 }); 154 } 155 } 156 157 _saveSelection() { 158 try { 159 const sel = document.getSelection(); 160 if (!sel || sel.rangeCount === 0) { 161 this._savedSelection = null; 162 return; 163 } 164 // Clone ranges because Range objects may be mutated later. 165 const ranges = []; 166 for (let i = 0; i < sel.rangeCount; i++) { 167 ranges.push(sel.getRangeAt(i).cloneRange()); 168 } 169 this._savedSelection = ranges; 170 } catch (err) { 171 // If any error occurs, don't block user â clear saved selection. 172 this._savedSelection = null; 173 } 174 } 175 176 _restoreSelection() { 177 if (!this._savedSelection) return; 178 try { 179 const sel = document.getSelection(); 180 if (!sel) return; 181 sel.removeAllRanges(); 182 for (const r of this._savedSelection) { 183 sel.addRange(r); 184 } 185 } catch (err) { 186 // ignore restore failures 187 } finally { 188 this._savedSelection = null; 189 } 190 } 191 192 _onCancel(e) { 193 e.preventDefault(); 194 this._restoreSelection(); 195 this.open = false;
196 } 197 198 _onOk(e) { 199 e.preventDefault(); 200 // Emit a custom event with the href value 201 this._restoreSelection(); 202 const value = {}; 203 this.querySelectorAll('[data-name]').forEach(input => { 204 value[input.getAttribute('data-name')] = input.value; 205 }); 206 this.dispatchEvent(new CustomEvent('rt-state', { 207 detail: { name: this.getAttribute('name'), value}, 208 bubbles: true, 209 composed: true 210 })); 211 this.open = false; 212 } 213 214 _onDocumentClick(e) { 215 // Close if clicking outside component 216 if (!this.contains(e.target) && this.open) { 217 this._restoreSelection(); 218 this.open = false; 219 } 220 } 221} 222 223customElements.define('mypip-richtext-dialog', RichtextDialog);
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.