PageSourceSearch

https://www.sfbar.org/wp-content/plugins/beaver-builder-lite-versi…/fl-layer-group.js?ver=2.10.3.2

js sfbar.org collected 2026-09-24 18:18:43 UTC 23,600 bytes, 915 lines download raw bytes

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.