1const toolboxIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 2 <rect x="2" y="4" width="20" height="16" rx="4" ry="4"/> 3 <polygon points="10,8 16,12 10,16" fill="currentColor" stroke="none"/> 4</svg>` 5 6export default class YouTube { 7 static get toolbox() { 8 return { 9 title: 'YouTube', 10 icon: toolboxIcon 11 } 12 } 13 14 static get isReadOnlySupported() { 15 return true 16 } 17 18 constructor({ data, config, api, readOnly }) { 19 this.api = api 20 this.readOnly = readOnly 21 this.data = { 22 url: data?.url || '', 23 videoId: data?.videoId || '' 24 } 25 } 26 27 render() { 28 this.wrapper = document.createElement('div') 29 this.wrapper.classList.add('youtube-tool') 30 31 if (this.data.videoId) { 32 this._renderVideo() 33 } else { 34 this._renderInput() 35 } 36 37 return this.wrapper 38 } 39 40 _renderInput() { 41 this.wrapper.innerHTML = '' 42 43 const container = document.createElement('div') 44 container.classList.add('p-4', 'border', 'border-dashed', 'border-gray-300', 'rounded-lg', 'bg-gray-50') 45 46 const label = document.createElement('div') 47 label.classList.add('text-xs', 'text-gray-400', 'mb-2') 48 label.textContent = 'Wklej link do YouTube' 49 50 this.urlInput = document.createElement('input') 51 this.urlInput.type = 'text' 52 this.urlInput.value = this.data.url 53 this.urlInput.placeholder = 'https://www.youtube.com/watch?v=...' 54 this.urlInput.disabled = this.readOnly 55 this.urlInput.classList.add( 56 'w-full', 'border', 'border-gray-200', 'rounded', 'px-3', 'py-2', 57 'text-sm', 'text-gray-700', 'focus:outline-none', 'focus:ring-2', 'focus:ring-blue-500' 58 ) 59 60 this.urlInput.addEventListener('paste', (e) => { 61 setTimeout(() => this._processUrl(this.urlInput.value), 0) 62 }) 63 64 this.urlInput.addEventListener('keydown', (e) => { 65 if (e.key === 'Enter') { 66 e.preventDefault() 67 this._processUrl(this.urlInput.value) 68 } 69 }) 70 71 const button = document.createElement('button') 72 button.type = 'button' 73 button.textContent = 'Dodaj wideo' 74 button.classList.add( 75 'mt-2', 'px-4', 'py-2', 'bg-red-600', 'text-white', 'rounded', 76 'text-sm', 'hover:bg-red-700', 'transition-colors' 77 ) 78 button.addEventListener('click', () => this._processUrl(this.urlInput.value)) 79 80 container.appendChild(label) 81 container.appendChild(this.urlInput) 82 container.appendChild(button) 83 this.wrapper.appendChild(container) 84 } 85 86 _renderVideo() { 87 this.wrapper.innerHTML = '' 88 89 const container = document.createElement('div') 90 container.classList.add('relative', 'w-full') 91 container.style.aspectRatio = '16 / 9' 92 93 const iframe = document.createElement('iframe') 94 iframe.src = `https://www.youtube.com/embed/${this.data.videoId}` 95 iframe.classList.add('w-full', 'h-full', 'rounded-lg') 96 iframe.setAttribute('frameborder', '0') 97 iframe.setAttribute('allowfullscreen', 'true') 98 iframe.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture') 99 100 container.appendChild(iframe) 101 this.wrapper.appendChild(container) 102 103 if (!this.readOnly) { 104 const removeBtn = document.createElement('button') 105 removeBtn.type = 'button' 106 removeBtn.innerHTML = '× UsuÅ' 107 removeBtn.classList.add( 108 'mt-2', 'px-3', 'py-1', 'bg-gray-200', 'text-gray-600', 'rounded', 109 'text-xs', 'hover:bg-gray-300', 'transition-colors' 110 ) 111 removeBtn.addEventListener('click', () => { 112 this.data.url = '' 113 this.data.videoId = '' 114 this._renderInput() 115 }) 116 this.wrapper.appendChild(removeBtn) 117 } 118 } 119 120 _processUrl(url) { 121 const videoId = this._extractVideoId(url) 122 if (videoId) { 123 this.data.url = url 124 this.data.videoId = videoId 125 this._renderVideo() 126 } 127 } 128 129 _extractVideoId(url) { 130 if (!url) return null 131 132 // Match various YouTube URL formats 133 const patterns = [ 134 /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/, 135 /^([a-zA-Z0-9_-]{11})$/ // Just the video ID 136 ] 137 138 for (const pattern of patterns) { 139 const match = url.match(pattern) 140 if (match) return match[1] 141 } 142 143 return null 144 } 145 146 save() { 147 return { 148 url: this.data.url, 149 videoId: this.data.videoId 150 } 151 } 152 153 static get sanitize() { 154 return { 155 url: false, 156 videoId: false 157 } 158 } 159}
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.