1/** 2 * Custom element class that represents an array of layer objects. 3 */ 4class FLLayerGroup extends FLElement { 5 6 defaultStrings = { 7 addNew: 'Add new TEST' 8 } 9 10 /** 11 * Array of element attributes to watch. 12 * 13 * @return Array 14 */ 15 static get observedAttributes() { 16 return [ 'value', 'name', 'insert-position' ] 17 } 18 19 /** 20 * Getter for name attribute 21 * 22 * @return String 23 */ 24 get name() { 25 return this.getAttribute( 'name' ) 26 } 27 28 /** 29 * Getter for value attribute 30 * 31 * @return String 32 */ 33 get value() { 34 return this.getAttribute( 'value' ) 35 } 36 37 get insertPosition() { 38 if ( this.hasAttribute( 'insert-position' ) ) { 39 return 'before' === this.getAttribute( 'insert-position' ) ? 'before' : 'after' 40 } 41 return 'after' 42 } 43 44 /** 45 * Setup Element shadowDOM and template 46 * 47 * @return void 48 */ 49 constructor() { 50 super() 51 this.attachShadow( { mode: 'open' } ) 52 } 53 /** 54 * Set up element after connected to DOM 55 * 56 * @return void 57 */ 58 connectedCallback() { 59 this.template.innerHTML = this.renderShadow() 60 this.shadowRoot.replaceChildren( this.template.content.cloneNode( true ) ) 61 this.unbindEventsCallback = this.bindEvents() 62 } 63 64 /** 65 * Fires in connectedCallback() after DOM renders 66 * 67 * @return void 68 */ 69 bindEvents() { 70 71 // Reorder List Items 72 const onReorder = this.onReorder.bind( this ) 73 const ul = this.shadowRoot.querySelector( 'ul' ) 74 jQuery( ul ).sortable( { 75 items: 'li', 76 cursor: 'move', 77 distance: 5, 78 opacity: 0.75, 79 placeholder: 'fl-layer-placeholder', 80 stop: onReorder, 81 tolerance: 'pointer', 82 axis: "y", 83 helper: 'clone', 84 } ) 85 86 // Header Buttons (add, clear) 87 const header = this.querySelector('fl-layer-group-heading') 88 header.layerGroup = this 89 const addLayer = this.addLayer.bind( this ) 90 const clearLayers = this.clearLayers.bind( this ) 91 header?.addEventListener( 'add', addLayer ) 92 header?.addEventListener( 'reset', clearLayers ) 93 94 // Remove Buttons (Layer Menu) 95 const removeLayer = this.removeLayer.bind( this ) 96 this.addEventListener( 'layer-remove', removeLayer ) 97 98 // Duplicate Buttons (Layer Menu) 99 const cloneLayer = this.cloneLayer.bind( this ) 100 this.addEventListener( 'layer-clone', cloneLayer ) 101 //const cloneButtons = this.shadowRoot.querySelectorAll( 'button.clone' ) 102 //cloneButtons.forEach( button => button.addEventListener( 'click', cloneLayer ) ) 103 104 // Listen for value changes on layer items 105 const onLayerChange = this.onLayerValueChange.bind( this ) 106 this.addEventListener( 'layer-change', onLayerChange ) 107 108 // Watch DOM mutations (like removing items) 109 const observer = new MutationObserver( ( list, observer ) => { 110 for ( const mutation of list ) { 111 if ( 'childList' === mutation.type ) { 112 if ( mutation.removedNodes.length ) { 113 this.updateValueAttribute() 114 } 115 } 116 } 117 } ) 118 observer.observe( ul, { childList: true } ) 119 120 // Unbind Events on disconnect 121 return () => { 122 jQuery( ul ).sortable( 'destroy' ) 123 124 header?.removeEventListener( 'add', addLayer ) 125 header?.removeEventListener( 'reset', clearLayers ) 126 127 //cloneButtons.forEach( button => button.removeEventListener( 'click', cloneLayer ) ) 128 this.removeEventListener( 'layer-remove', removeLayer ) 129 this.removeEventListener( 'layer-clone', cloneLayer ) 130 this.removeEventListener( 'layer-change', onLayerChange ) 131 132 observer.disconnect() 133 } 134 } 135 136 supportedLayerTypes() { 137 return [ 'text', 'basic-track' ] 138 } 139 140 supportsMultipleLayerTypes() { 141 return this.supportedLayerTypes().length > 1 142 } 143 144 /** 145 * Get all layer items 146 * 147 * @return NodeList 148 */ 149 getListElements() { 150 const selector = 'li:not(.ui-sortable-helper, .fl-layer-placeholder) > fl-layer-group-item' 151 return this.shadowRoot.querySelectorAll( selector ) 152 } 153 154 /** 155 * Reset value attribute based on values of each child layer. 156 * 157 * @return void 158 */ 159 updateValueAttribute() { 160 const newValue = JSON.stringify( this.getLayerValues() ) 161 this.setAttribute( 'value', newValue ) 162 this.dispatchEvent( new Event( 'change' ) ) 163 } 164 165 /** 166 * Handle updating value after sorting the list 167 * 168 * @return void 169 */ 170 onReorder() {
171 this.updateValueAttribute() 172 } 173 174 /** 175 * Listen for value changes on each child layer 176 * 177 * @return void 178 */ 179 onLayerValueChange() { 180 this.updateValueAttribute() 181 } 182 183 /** 184 * Get array of all layer objects 185 * 186 * @return Array 187 */ 188 getLayerValues() { 189 return Array.from( this.getListElements() ).map( item => { 190 return JSON.parse( item.getAttribute('value') ) 191 } ) 192 } 193 194 getDefaultLayerProps() { 195 if ( this.hasAttribute( 'default' ) && '' !== this.getAttribute( 'default' ) ) { 196 return JSON.parse( this.getAttribute( 'default' ) ) 197 } else { 198 return { type: 'text', value: '' } 199 } 200 } 201 202 addLayer( e ) { 203 const ul = this.shadowRoot.querySelector( 'ul' ) 204 205 const template = document.createElement( 'template' ) 206 template.innerHTML = this.renderLayer( e.detail ) 207 const node = template.content.cloneNode( true ) 208 209 if ( 'before' === this.insertPosition ) { 210 ul.prepend( node ) 211 } else { 212 ul.append( node ) 213 } 214 215 this.setAttribute( 'value', JSON.stringify( this.getLayerValues() ) ) 216 this.dispatchEvent( new Event( 'change' ) ) 217 } 218 219 cloneLayer( e ) { 220 const layer = e.detail 221 const template = document.createElement( 'template' ) 222 template.innerHTML = this.renderLayer( { ...layer.value } ) 223 const node = template.content.cloneNode( true ) 224 layer.parentElement.after( node ) 225 layer.parentElement.nextElementSibling?.querySelector('input')?.focus() 226 this.updateValueAttribute() 227 } 228 229 removeLayer( e ) { 230 const item = e.detail.closest('li') 231 232 // Locate next item before we remove the current one 233 let next = null 234 if ( item?.nextElementSibling ) { 235 next = item?.nextElementSibling 236 } else if ( item?.previousElementSibling ) { 237 next = item?.previousElementSibling 238 } 239 240 item?.remove() 241 242 // Focus next item 243 next?.querySelector('input,select')?.focus() 244 } 245 246 clearLayers() { 247 const ul = this.shadowRoot.querySelector( 'ul' ) 248 while ( ul.firstChild ) { 249 ul.removeChild( ul.firstChild ) 250 } 251 252 // Empty placeholder?? 253 // Create new layer node 254 const template = document.createElement( 'template' ) 255 template.innerHTML = this.renderLayer( this.getDefaultLayerProps() ) 256 const node = template.content.cloneNode( true ) 257 ul.appendChild( node ) 258 ul.querySelector( 'input' )?.focus() 259 260 this.setAttribute( 'value', JSON.stringify( this.getLayerValues() ) ) 261 this.dispatchEvent( new Event( 'change' ) ) 262 } 263 264 /** 265 * Render single list item html string 266 * 267 * @return String 268 */ 269 renderLayer( props ) { 270 const value = JSON.stringify( props ) 271 const strings = JSON.stringify( this.strings ) 272 return ` 273 <li> 274 <fl-layer-group-item 275 value='${ value }' 276 strings='${ strings }' 277 > 278 </fl-layer-group-item> 279 </li> 280 ` 281 } 282 283 renderDefaultLayer() { 284 return this.renderLayer( this.getDefaultLayerProps() ) 285 } 286 287 /** 288 * Assemble html string to be inserted into the shadow root. 289 * 290 * @return String 291 */ 292 renderShadow() { 293 294 // Value should contain a serialized array of layer objects 295 let layers = [] 296 if ( '' !== this.value ) { 297 layers = JSON.parse( this.value ) 298 } 299 return ` 300 <style> 301 :host { 302 box-sizing: border-box; 303 box-shadow: var(--fl-builder-input-shadow); 304 } 305 :host-context(.fl-layer-group-cluster) { 306 border-radius: 0; 307 } 308 ul { 309 display: grid; 310 list-style: none; 311 margin: 0; 312 padding: 0; 313 gap: 1px; 314 background: var(--fl-builder-outline-color); 315 } 316 li { 317 display: grid; 318 position: relative; 319 background: var(--fl-builder-input-bg-color); 320 min-height: var(--fl-builder-target-size); 321 } 322 li:focus-within { 323 box-shadow: var(--fl-builder-focus-shadow); 324 } 325 .fl-layer-placeholder { 326 min-height: var(--fl-builder-target-size); 327 } 328 button { 329 border: none; 330 background: transparent; 331 outline: none; 332 font-size: inherit; 333 color: var(--fl-builder-dim-color); 334 } 335 .handle { 336 display: flex; 337 align-items: center; 338 justify-content: center; 339 } 340 .handle, fl-layer-menu { 341 opacity: 0; 342 } 343 li:hover .handle, 344 li:focus-within .handle, 345 li:hover fl-layer-menu,
346 li:focus-within fl-layer-menu, 347 fl-layer-menu[visible] { 348 opacity: 1; 349 } 350 </style> 351 <slot name="before"></slot> 352 <ul> 353 ${ 0 >= layers.length ? this.renderDefaultLayer() : '' } 354 ${ 0 < layers.length ? layers.map( layer => this.renderLayer( layer ) ).join('') : '' } 355 </ul> 356 <slot name="after"></slot> 357 ` 358 } 359} 360customElements.define( "fl-layer-group", FLLayerGroup ) 361 362const layerTypes = { 363 default: { 364 label: "Layer", 365 366 bindEvents( layer ) { 367 const input = layer.querySelector( 'input' ) 368 const onInput = this.onInput.bind( layer ) 369 input?.addEventListener( 'input', onInput ) 370 371 this.layer = layer 372 const onKeyUp = this.onKeyUp.bind( this ) 373 input?.addEventListener( 'keyup', onKeyUp ) 374 375 // Track previous value for backspace 376 const onKeyDown = e => this.lastValue = e.target.value 377 input?.addEventListener( 'keydown', onKeyDown ) 378 379 // Cleanup 380 return () => { 381 input?.removeEventListener( 'input', onInput ) 382 input?.removeEventListener( 'keyup', onKeyUp ) 383 input?.removeEventListener( 'keydown', onKeyDown ) 384 } 385 }, 386 387 onInput( e ) { 388 const state = this.value 389 this.value = { ...state, value: e.target.value } 390 }, 391 392 onKeyUp( e ) { 393 const layerGroup = this.layer.getRootNode().host 394 395 switch( e.key ) { 396 case 'Enter': 397 const isCursorAtEnd = e.target.value.length === e.target.selectionStart 398 if ( isCursorAtEnd && '' !== e.target.value ) { 399 400 // Create new layer node 401 const template = document.createElement( 'template' ) 402 template.innerHTML = layerGroup.renderLayer( { type: 'text', value: '' } ) 403 const node = template.content.cloneNode( true ) 404 this.layer.parentElement.after( node ) 405 this.layer.parentElement.nextElementSibling.querySelector('input')?.focus() 406 } 407 break; 408 case 'Backspace': 409 if ( '' === this.lastValue && 1 < layerGroup.getListElements().length ) { 410 this.layer.delete() 411 } 412 break; 413 } 414 }, 415 416 render( props, type, strings ) { 417 const placeholder = strings.addNew 418 return `<input type="text" value='${ props.value }' placeholder='${ placeholder }' />` 419 }, 420 }, 421 422 // Text type inherits all the default functionality 423 text: { 424 label: "Text", 425 }, 426 427 // Grid Track - produces repeat( count, 1fr ) 428 'basic-track': { 429 label: "Repeat", 430 bindEvents( layer ) { 431 this.layer = layer 432 const stepperInput = layer.querySelector( 'input' ) 433 stepperInput?.addEventListener( 'change', this.onChange.bind( layer ) ) 434 }, 435 onChange( e ) { 436 this.value = { ...this.value, value: e.target.value } 437 }, 438 render( props, type ) { 439 return ` 440 <div style="display: flex; align-items: center; font-size: 13px"> 441 <span>Number of Tracks:</span> 442 <fl-stepper data-name="" min="0"> 443 <input type="hidden" name="" value="${ props.value }" /> 444 </fl-stepper> 445 </div> 446 ` 447 } 448 }, 449} 450 451class FLLayerGroupItem extends FLElement { 452 453 defaultProps = { 454 type: 'default', 455 value: 'Untitled', 456 } 457 458 connectedCallback() { 459 this.template.innerHTML = this.render() 460 this.replaceChildren( this.template.content.cloneNode( true ) ) 461 this.unbindEventsCallback = this.bindEvents() 462 } 463 464 bindEvents() { 465 466 this.layerMenu = this.querySelector( 'fl-layer-menu' ) 467 468 // Bind Layer Type Object 469 const layerType = this.getType( this.getProps()?.type ) 470 const layerTypeUnbindCallback = layerType?.bindEvents( this ) 471 472 // Duplicate Button 473 const onCloneClicked = this.onCloneClicked.bind(this) 474 const cloneButton = this.querySelector( 'button.clone' ) 475 cloneButton?.addEventListener( 'click', onCloneClicked ) 476 477 // Remove Button 478 const onRemoveClicked = this.onRemoveClicked.bind(this) 479 const removeButton = this.querySelector( 'button.remove' ) 480 removeButton?.addEventListener( 'click', onRemoveClicked ) 481 482 return () => { 483 484 // Handle unbinding for the item's layer type object 485 if ( 'function' === typeof layerTypeUnbindCallback ) { 486 layerTypeUnbindCallback() 487 } 488 489 cloneButton?.removeEventListener( 'click', onCloneClicked ) 490 removeButton?.removeEventListener( 'click', onRemoveClicked ) 491 } 492 } 493 494 onRemoveClicked() { 495 this.dispatchEvent( new CustomEvent( 'layer-remove', { 496 detail: this, 497 bubbles: true, 498 composed: true, 499 } ) ) 500 } 501 502 onCloneClicked() { 503 this.dispatchEvent( new CustomEvent( 'layer-clone', { 504 detail: this, 505 bubbles: true, 506 composed: true, 507 } ) ) 508 this.layerMenu?.close() 509 } 510 511 get value() { 512 return JSON.parse( this.getAttribute( 'value' ) ) 513 } 514 515 set value( newValue ) { 516 this.setAttribute( 'value', JSON.stringify( newValue ) ) 517 this.dispatchEvent( new Event( 'layer-change', { 518 bubbles: true, 519 composed: true, 520 } ) ) 521 } 522 523 getType( name ) { 524 const types = layerTypes 525 const defaultType = layerTypes.default 526 527 if ( name in types ) { 528 return { ...defaultType, ...types[name] } 529 } 530 return defaultType 531 } 532 533 getProps() { 534 return { ...this.defaultProps, ...this.value } 535 } 536 537 delete() { 538 const previous = this.parentElement.previousElementSibling 539 540 // Focus previous input and place cursor at the end 541 const prevInput = previous?.querySelector(':is(input)') 542 const end = prevInput?.value.length 543 544 if ( 'hidden' !== prevInput.getAttribute('type') ) { 545 prevInput?.setSelectionRange(end, end) 546 prevInput?.focus() 547 } 548 549 // Remove the layer 550 this.parentElement.remove() 551 } 552 553 get strings() { 554 return { 555 ...super.strings, 556 ...JSON.parse( this.getAttribute( 'strings' ) ) 557 } 558 } 559 560 render() { 561 const props = this.getProps() 562 const type = this.getType( props.type ) 563 const strings = this.strings 564 565 return ` 566 <style>
567 fl-layer-group-item { 568 display: grid; 569 grid-template-columns: 32px 1fr 32px; 570 cursor: move; 571 } 572 :where( input, select ) { 573 color: var(--fl-builder-dim-color); 574 border: none; 575 background: transparent; 576 } 577 :where(input,select):focus { 578 border: none; 579 outline: none; 580 } 581 .handle { 582 pointer-events: none; 583 } 584 </style> 585 <button class="handle"> 586 <svg width="20" height="20" viewBox="0 0 20 20" fill="none"> 587 <path d="M11.7637 3.57913C12.0439 4.00296 11.8867 4.48147 11.374 4.48147H8.64648C8.12012 4.48147 7.95605 4.01663 8.25684 3.57913L9.54883 1.67874C9.77441 1.34378 10.2666 1.3301 10.4922 1.67874L11.7637 3.57913ZM5.67969 14.4756C4.24414 14.4756 3.5332 13.7715 3.5332 12.3565V7.64651C3.5332 6.23147 4.24414 5.52737 5.67969 5.52737H14.3203C15.7559 5.52737 16.4668 6.23831 16.4668 7.64651V12.3565C16.4668 13.7647 15.7559 14.4756 14.3203 14.4756H5.67969ZM5.69336 13.375H14.3066C14.9834 13.375 15.3662 13.0059 15.3662 12.3018V7.7012C15.3662 6.99026 14.9834 6.62796 14.3066 6.62796H5.69336C5.00977 6.62796 4.63379 6.99026 4.63379 7.7012V12.3018C4.63379 13.0059 5.00977 13.375 5.69336 13.375ZM11.7637 16.417L10.4922 18.3242C10.2666 18.666 9.77441 18.6592 9.54883 18.3242L8.25684 16.417C7.95605 15.9795 8.12012 15.5215 8.64648 15.5215H11.374C11.8867 15.5215 12.0439 16 11.7637 16.417Z" fill="currentColor"/> 588 </svg> 589 </button> 590 ${ type.render( props, type, strings ) } 591 <fl-layer-menu> 592 <button class="clone">${strings.clone}</button> 593 <button class="remove">${strings.delete}</button> 594 </fl-layer-menu> 595 ` 596 } 597} 598customElements.define( "fl-layer-group-item", FLLayerGroupItem ) 599 600 601class FLLayerGroupToolbar extends FLElement { 602 603 defaultStrings = { 604 clear: 'Clear', 605 addNew: 'Add New Layer', 606 } 607 608 constructor() { 609 super() 610 this.attachShadow( { mode: 'open' } ) 611 } 612 613 connectedCallback() { 614 this.template.innerHTML = this.renderShadowRoot() 615 this.shadowRoot.replaceChildren( this.template.content.cloneNode( true ) ) 616 this.unbindEventsCallback = this.bindEvents() 617 } 618 619 bindEvents() { 620 this.addMenu = this.shadowRoot.querySelector( '.add-menu' ) 621 622 const addButton = this.shadowRoot.querySelector('.add') 623 const addClicked = this.addClicked.bind(this) 624 addButton?.addEventListener( 'click', addClicked ) 625 626 const clearButton = this.shadowRoot.querySelector('.clear') 627 const clearClicked = this.resetClicked.bind(this) 628 clearButton?.addEventListener( 'click', clearClicked ) 629 630 const menuButtonClicked = this.menuButtonClicked.bind(this) 631 const menuButtons = this.shadowRoot.querySelectorAll('.add-menu button') 632 menuButtons?.forEach( button => button.addEventListener( 'click', menuButtonClicked ) ) 633 634 return () => { 635 addButton?.removeEventListener( 'click', addClicked ) 636 clearButton?.removeEventListener( 'click', clearClicked ) 637 menuButtons?.forEach( button => button.removeEventListener( 'click', menuButtonClicked ) ) 638 } 639 } 640 641 addClicked() { 642 if ( this.layerGroup.supportsMultipleLayerTypes() ) { 643 this.toggleMenu() 644 } else { 645 this.dispatchEvent( new CustomEvent( 'add', { 646 detail: { 647 type: 'text', 648 value: '' 649 } 650 } ) ) 651 } 652 } 653 654 resetClicked() { 655 this.dispatchEvent( new Event( 'reset' ) ) 656 } 657 658 menuButtonClicked( e ) { 659 const type = JSON.parse( e.target.getAttribute( 'data-type' ) ) 660 this.dispatchEvent( new CustomEvent( 'add', { detail: type } ) ) 661 this.hideMenu() 662 } 663 664 showMenu() { 665 this.addMenu?.open() 666 return 667 } 668 669 hideMenu() { 670 this.addMenu?.close() 671 } 672 673 toggleMenu() { 674 this.addMenu?.isOpen() ? this.addMenu?.close() : this.addMenu?.open() 675 } 676 677 renderAddMenu() { 678 const items = [ 679 { 680 label: 'Multiple Tracks', 681 props: { type: 'basic-track', value: '3' }, 682 }, 683 { 684 label: 'Freeform Text', 685 props: { type: 'text', value: '' }, 686 }, 687 { 688 label: 'Auto', 689 props: { type: 'text', value: 'auto' }, 690 }, 691 { 692 label: '1fr', 693 props: { type: 'text', value: '1fr' }, 694 }, 695 ] 696 return ` 697 <fl-menu class="add-menu"> 698 ${ items.map( item => { 699 return `<button data-type='${ JSON.stringify( item.props ) }'>${ item.label }</button>` 700 } ).join('') } 701 </fl-menu> 702 ` 703 } 704
705 renderShadowRoot() { 706 return ` 707 <style> 708 :host { 709 --height: 24px; 710 display: flex; 711 background: var(--fl-builder-heading-bg-color); 712 height: var(--height); 713 border-bottom: 1px solid var(--fl-builder-outline-color); 714 } 715 * { 716 font-size: 12px; 717 color: var(--fl-builder-dim-color); 718 } 719 div.label { 720 721 flex-grow: 1; 722 display: flex; 723 align-items: center; 724 padding: 0 10px; 725 } 726 :where( :host > button, .add-wrap > button ) { 727 border: none; 728 background: inherit; 729 outline: none; 730 font-size: inherit; 731 display: flex; 732 place-items: center; 733 place-content: center; 734 } 735 button.add { 736 min-width: 32px; 737 display: flex; 738 } 739 740 :where( :host > button, .add-wrap > button ):hover, 741 :where( :host > button, .add-wrap > button ):host > button:focus-visible { 742 background: var(--fl-builder-platter-bg-color); 743 } 744 :where( :host > button, .add-wrap > button ):focus-visible { 745 box-shadow: var(--fl-builder-focus-shadow); 746 color: var(--fl-builder-accent-color); 747 } 748 :where( :host > button, .add-wrap > button ) * { 749 pointer-events: none; 750 } 751 752 .add-wrap { 753 position:relative; 754 display: grid; 755 } 756 .add-wrap:hover { 757 background: var(--fl-builder-input-bg-color); 758 } 759 </style> 760 <div class="label"><slot></slot></div> 761 <button class="clear">${ this.strings.clear }</button> 762 <div class="add-wrap"> 763 <button class="add" title='${ this.strings.addNew }'> 764 <svg width="15" height="20" viewBox="0 0 15 20" fill="none"> 765 <path d="M13.1569 10.0001H1.84315" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" /> 766 <path d="M7.5 4.34326V15.657" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> 767 </svg> 768 </button> 769 ${ this.renderAddMenu() } 770 </div> 771 ` 772 } 773} 774customElements.define( "fl-layer-group-heading", FLLayerGroupToolbar ) 775 776 777class FLLayerMenu extends FLElement { 778 779 // cached for adding/removeing listeners without creating new functions each time 780 dismissCallback = this.maybeDismissMenu.bind(this) 781 782 static get observedAttributes() { 783 return [ 'visible' ] 784 } 785 786 constructor() { 787 super() 788 this.attachShadow( { mode: 'open' } ) 789 this.template = document.createElement( 'template' ) 790 } 791 792 connectedCallback() { 793 this.template.innerHTML = this.renderShadow() 794 this.shadowRoot.replaceChildren( this.template.content.cloneNode( true ) ) 795 this.unbindEventsCallback = this.bindEvents() 796 797 if ( this.hasAttribute( 'visible' ) ) { 798 this.showMenu() 799 } 800 } 801 802 attributeChangedCallback( name, oldValue, newValue ) { 803 if ( 'visible' === name ) { 804 null === newValue ? this.hideMenu() : this.showMenu() 805 } 806 } 807 808 bindEvents() { 809 this.button = this.shadowRoot.querySelector('button') 810 const toggle = this.toggleVisible.bind(this) 811 this.button.addEventListener( 'click', toggle ) 812 813 return () => { 814 this.button.removeEventListener( 'click', toggle ) 815 document.removeEventListener( 'click', this.dismissCallback ) 816 document.removeEventListener( 'keyup', this.dismissCallback ) 817 } 818 } 819 820 toggleVisible() { 821 this.hasAttribute( 'visible' ) ? this.close() : this.open() 822 } 823 824 close() { 825 this.removeAttribute( 'visible' ) 826 } 827 828 open() { 829 this.setAttribute( 'visible', '' ) 830 } 831 832 /** 833 * Dismiss the menu if either clicking outside or hitting escape. 834 */ 835 maybeDismissMenu( e ) { 836 if ( ! e.composedPath().includes( this ) || 'Escape' === e.key ) { 837 this.removeAttribute( 'visible' ) 838 } 839 } 840 841 showMenu() { 842 const menuTemplate = document.createElement( 'template' ) 843 menuTemplate.innerHTML = this.renderMenu() 844 this.shadowRoot.appendChild( menuTemplate.content.cloneNode( true ) ) 845 846 // Add listener for click away 847 document.addEventListener( 'click', this.dismissCallback ) 848 document.addEventListener( 'keyup', this.dismissCallback ) 849 } 850 851 hideMenu() { 852 const menu = this.shadowRoot.querySelector( '.menu' ) 853 menu?.remove() 854 855 // Remove click away listener 856 document.removeEventListener( 'click', this.dismissCallback ) 857 document.removeEventListener( 'keyup', this.dismissCallback ) 858 } 859 860 renderMenu() { 861 return ` 862 <div class="menu"> 863 <slot></slot> 864 </div> 865 ` 866 } 867
868 renderShadow() { 869 return ` 870 <style> 871 :host { 872 flex-grow: 1; 873 display: flex; 874 } 875 .label { 876 flex-grow: 1; 877 display: flex; 878 align-items: center; 879 padding: 0 10px; 880 } 881 button { 882 border: none; 883 background: inherit; 884 outline: none; 885 font-size: inherit; 886 color: inherit; 887 display: flex; 888 place-items: center; 889 color: var(--fl-builder-dim-color); 890 } 891 .menu { 892 display: grid; 893 grid-auto-rows: var(--fl-builder-target-size); 894 gap: 1px; 895 background: var(--fl-builder-input-bg-color); 896 box-shadow: var(--fl-builder-input-shadow), 0 10px 20px hsla(0deg, 0%, 0%, .25); 897 position: absolute; 898 top: calc( 100% + 1px ); 899 right: 0; 900 width: calc( 100% - 32px ); 901 max-width: 120px; 902 z-index: 9; 903 } 904 </style> 905 <button> 906 <svg width="20" height="21" viewBox="0 0 20 21" fill="none"> 907 <path d="M10 15.2861C8.895 15.2861 8 16.1811 8 17.2861C8 18.3911 8.895 19.2861 10 19.2861C11.105 19.2861 12 18.3911 12 17.2861C12 16.1811 11.105 15.2861 10 15.2861Z" fill="currentColor"/> 908 <path d="M10 1.28613C8.895 1.28613 8 2.18113 8 3.28613C8 4.39113 8.895 5.28613 10 5.28613C11.105 5.28613 12 4.39113 12 3.28613C12 2.18113 11.105 1.28613 10 1.28613Z" fill="currentColor"/> 909 <path d="M10 8.28613C8.895 8.28613 8 9.18113 8 10.2861C8 11.3911 8.895 12.2861 10 12.2861C11.105 12.2861 12 11.3911 12 10.2861C12 9.18113 11.105 8.28613 10 8.28613Z" fill="currentColor"/> 910 </svg> 911 </button> 912 ` 913 } 914} 915customElements.define( "fl-layer-menu", FLLayerMenu )
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.