1/* global nn, NetizenBentText, Netitor */ 2class NetizenBendPanel { 3 constructor (opts = {}) { 4 this.showing = false // displaying the bend panel itself 5 6 // parent for the clickable canvas text 7 this.textEle = opts.textEle 8 this.panelEle = opts.panelEle 9 10 this.text = new NetizenBentText({ 11 parent: this.textEle, 12 text: ['BEND', 'THIS', 'SITE'], 13 color: nn.get('.grid div:nth-child(2)').style.backgroundColor, 14 padding: 10, 15 xOverlap: 30, 16 yOverlap: 80 17 }) 18 19 this.text.canvas.style.cursor = 'pointer' 20 this.text.canvas.style.transition = 'all var(--tt) var(--out-cubic)' 21 this.text.canvas.onclick = () => this.togglePanel() 22 23 // element for the bend panel itself 24 this.panel = nn.create('div').set('class', 'bend-panel').addTo(this.panelEle) 25 this.panel.innerHTML = ` 26 <div class="options"> 27 <div class="row"> 28 <button type="button" name="close" class="sec-nav">close panel</button> 29 <button type="button" name="reset" class="sec-nav">reset code</button> 30 <small style="margin-left: 20px;">THIS IS IN BETA (BUGS ABOUND)</small> 31 </div> 32 </div> 33 <div id="editor"></div> 34 ` 35 36 this._setupOptions() 37 this._setupEditor() 38 this.panel.remove() 39 40 this.onShow = opts.onShow 41 this.onHide = opts.onHide 42 } 43 44 getCode () { 45 // let str = '\n /*\n edit this site\'s CSS using the controls above\n or modifying the code below \n */\n main {\n' 46 let str = '\n main {\n' 47 Object.entries(this.cssObj).forEach(a => { 48 str += ` ${a[0]}: ${a[1]};\n` 49 }) 50 str += ' }' 51 return str 52 } 53 54 enable () { 55 // if it's been removed, add it back in 56 if (this.textEle.children.length === 0) { 57 this.textEle.appendChild(this.text.canvas) 58 } 59 60 if (this.text.canvas.style.display === 'block' && 61 this.text.canvas.style.opacity === 1) return 62 63 // display the text canvas 64 this.text.canvas.style.display = 'block' 65 setTimeout(() => { 66 this.text.canvas.style.opacity = 1 67 }, 100) 68 } 69 70 disable () { 71 if (this.text.canvas.style.display === 'none' && 72 this.text.canvas.style.opacity === 0) return 73 74 this.text.canvas.style.opacity = 0 75 setTimeout(() => { 76 this.text.canvas.style.display = 'none' 77 }, 800) 78 } 79 80 showPanel () { 81 if (this.showing) return 82 if (typeof this.onShow === 'function') this.onShow() 83 this.panel.css('display', 'block') 84 setTimeout(() => this.panel.css('opacity', 1), 100) 85 this.panel.addTo(this.panelEle) 86 setTimeout(() => { this.ne.code = this.getCode() }, 800) 87 this.showing = true 88 } 89 90 hidePanel () { 91 if (!this.showing) return 92 this.panel.remove() 93 this.panel.css('display', 'none') 94 this.panel.css('opacity', 0) 95 if (typeof this.onHide === 'function') this.onHide() 96 this.showing = false 97 } 98 99 togglePanel () { 100 if (this.showing) this.hidePanel() 101 else this.showPanel() 102 } 103 104 reset () { 105 nn.get('main').removeAttribute('style') 106 this.filters = {} 107 this.cssObj = JSON.parse(JSON.stringify(this.originalCSS)) 108 this.ne.code = this.getCode() 109 Object.entries(this.defaultFilts).forEach(a => { 110 nn.get(`#bp-${a[0]}`).checked = false 111 if (this._hasRange(a[0])) { 112 nn.get(`input[name="bp-${a[0]}"]`).value = a[1].def 113 nn.get(`input[name="bp-${a[0]}"]`).css('display', 'none') 114 } 115 }) 116 } 117 118 // ........................................................................... 119 120 _setupOptions () { 121 this.filters = {} 122 123 this.defaultFilts = { 124 contrast: { min: 0, max: 2, step: 0.01, def: 1 }, 125 // brightness: { min: 0, max: 2, step: 0.01, def: 1 }, 126 saturate: { min: 0, max: 2, step: 0.01, def: 1 }, 127 // invert: { min: 0, max: 1, step: 1, def: 0 } 128 invert: { def: 0 } 129 } 130 131 const updateFiltCSS = (e, name) => { 132 if (e) this.filters[name] = e.target.value 133 if (Object.entries(this.filters).length > 0) { 134 this.cssObj.filter = this._buildFilterString(this.filters) 135 } else { 136 delete this.cssObj.filter 137 } 138 nn.get('main').removeAttribute('style') 139 this.ne.code = this.getCode() 140 } 141 142 const toggleFiltCSS = (e, name) => { 143 if (e.target.checked) { 144 if (this._hasRange(name)) { 145 nn.get(`input[name="bp-${name}"]`).css('display', 'inline') 146 } 147 this.filters[name] = this._hasRange(name) ? this.defaultFilts[name].def : 1 148 if (!this._hasRange(name)) updateFiltCSS() 149 } else { 150 if (this._hasRange(name)) { 151 nn.get(`input[name="bp-${name}"]`).css('display', 'none') 152 } 153 delete this.filters[name] 154 updateFiltCSS() 155 } 156 } 157 158 // .... create controls 159
160 Object.entries(this.defaultFilts).forEach(a => { 161 const f = nn.create('div').set('class', 'row').addTo('.bend-panel .options') 162 const n = a[0] 163 const v = a[1] 164 f.innerHTML = ` 165 <label class="toggle-switch"> 166 <input type="checkbox" id="bp-${n}"> 167 <span class="slider"></span> 168 </label> 169 <span class="label">${n}</span> 170 ` 171 if (v.step) { 172 f.innerHTML += ` 173 <input type="range" name="bp-${n}" 174 min="${v.min}" max="${v.max}" step="${v.step}" value="${v.def}"> 175 ` 176 } 177 }) 178 179 // ..... setup listeners 180 181 nn.get('.bend-panel button[name="close"]') 182 .on('click', () => this.hidePanel()) 183 184 nn.get('.bend-panel button[name="reset"]') 185 .on('click', () => this.reset()) 186 187 Object.entries(this.defaultFilts).forEach(a => { 188 nn.get(`#bp-${a[0]}`) 189 .on('change', e => toggleFiltCSS(e, a[0])) 190 if (this._hasRange(a[0])) { 191 nn.get(`input[name="bp-${a[0]}"]`) 192 .css('display', 'none') 193 .on('input', e => updateFiltCSS(e, a[0])) 194 } 195 }) 196 } 197 198 _setupEditor () { 199 this.cssObj = { 200 'font-family': "'Zen Maru Gothic', serif" 201 } 202 203 this.originalCSS = JSON.parse(JSON.stringify(this.cssObj)) 204 205 this.ne = new Netitor({ 206 ele: '#editor', 207 theme: 'sonnenzimmer', 208 hint: false, 209 language: 'css', 210 code: this.getCode() 211 }) 212 213 this.ne.on('code-update', () => { 214 this.cssObj = this._parseCSSClass(this.ne.code) 215 // update <main> styles && .case-study-detail styles 216 Object.entries(this.cssObj).forEach(a => nn.get('main').css(a[0], a[1])) 217 // bi-directional editing (upate UI) 218 if (this.cssObj.filter) { 219 const filts = this._parseFilter(this.cssObj.filter) 220 Object.entries(filts).forEach(a => { this.filters[a[0]] = a[1] }) 221 this._updateFilterUI(this.filters) 222 } 223 }) 224 } 225 226 // ........................................................................... 227 228 _updateFilterUI (obj) { 229 if (obj) this.filters = obj 230 Object.entries(this.defaultFilts).forEach(a => { 231 if (this.filters[a[0]]) { 232 nn.get(`#bp-${a[0]}`).checked = this.filters[a[0]] 233 if (this._hasRange(a[0])) { 234 nn.get(`input[name="bp-${a[0]}"]`).value = parseFloat(this.filters[a[0]]) 235 } 236 } else { 237 nn.get(`#bp-${a[0]}`).checked = false 238 if (this._hasRange(a[0])) { 239 nn.get(`input[name="bp-${a[0]}"]`).value = a[1].def 240 } 241 } 242 }) 243 } 244 245 _hasRange (name) { 246 return typeof this.defaultFilts[name].step === 'number' 247 } 248 249 _getCSSVar (varname) { 250 const name = varname.startsWith('--') ? varname : `--${varname}` 251 return window.getComputedStyle(document.documentElement) 252 .getPropertyValue(name) 253 .trim() 254 } 255 256 _parseCSSClass (cssString) { 257 // find the content between the first `{` and the last `}` 258 const start = cssString.indexOf('{') 259 const end = cssString.lastIndexOf('}') 260 if (start < 0 || end < 0) return {} 261 const body = cssString.slice(start + 1, end) 262 263 const result = {} 264 body.split(';').forEach(decl => { 265 const [prop, ...valParts] = decl.split(':') 266 if (!valParts.length) return 267 const key = prop.trim() 268 const value = valParts.join(':').trim() 269 if (key) result[key] = value 270 }) 271 272 return result 273 } 274 275 _parseFilter (filterString) { 276 const regex = /([a-z-]+)\(([^)]+)\)/gi 277 const result = {} 278 let match 279 280 while ((match = regex.exec(filterString))) { 281 const name = match[1] 282 const value = match[2].trim() 283 284 if (result[name]) { 285 // if already seen, convert to array (or push into it) 286 if (Array.isArray(result[name])) { 287 result[name].push(value) 288 } else { 289 result[name] = [result[name], value] 290 } 291 } else { 292 result[name] = value 293 } 294 } 295 296 return result 297 } 298 299 _buildFilterString (filters) { 300 const parts = [] 301 Object.entries(filters).forEach(([name, value]) => { 302 if (Array.isArray(value)) { 303 value.forEach(v => { 304 parts.push(`${name}(${v})`) 305 }) 306 } else { 307 parts.push(`${name}(${value})`) 308 } 309 }) 310 return parts.join('\n ') 311 } 312} 313 314window.NetizenBendPanel = NetizenBendPanel
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.