PageSourceSearch

https://netizen.org/js/netizen-bend-panel.js

js netizen.org collected 2026-10-03 23:33:45 UTC 9,020 bytes, 314 lines download raw bytes

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.