Completed
Push — master ( d0d7d8...965e58 )
by greg
04:24 queued 01:45
created

EditorAutocomplete.js ➔ ???   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 49
Bugs 27 Features 0
Metric Value
cc 1
c 49
b 27
f 0
nc 1
nop 0
dl 0
loc 23
rs 9.0856
1
/*global document, json */
2
3
import EditorUtils from '../modules/EditorUtils'
4
import Json from '../modules/EditorJson'
5
import {IframeCommentNode} from '../utils/iframe'
6
import Handlebars from 'handlebars'
7
import Nanoajax from 'nanoajax'
8
import on from 'on'
9
import qs from 'qs'
10
11
export default class EditorAutocomplete {
12
  constructor() {
13
    this._ajax = Nanoajax.ajax
14
    this._json = Json.instance
15
    this.onReload = on(this)
16
    this._previousValue = ''
17
18
    this._handleKeyUp = this._keyUp.bind(this)
19
    this._handleKeyDown = this._keyDown.bind(this)
20
    this._handleFocus = this._focus.bind(this)
21
    this._handleBlur = this._blur.bind(this)
22
    this._handleRemove = this._remove.bind(this)
23
    this._handleDocumentClick = this._documentClick.bind(this)
24
    this._handleSelectValue = this._selectValue.bind(this)
25
    this._handleRefresh = this._refresh.bind(this)
26
27
    this._currentInput = null
28
    this._divWrapper = document.createElement('div')
29
    this._divWrapper.classList.add('autocomplete-wrapper')
30
31
    this._visible = false
32
33
    this.rebind()
34
  }
35
36
  rebind() {
37
    this._autocompletesRemove = [].slice.call(document.querySelectorAll('[data-autocomplete-remove=true]'))
38
    this._autocompletes = [].slice.call(document.querySelectorAll('[data-autocomplete=true]'))
39
    this._autocompletesRefresh = [].slice.call(document.querySelectorAll('[data-autocomplete-refresh=true]'))
40
41
    document.body.removeEventListener('mouseup', this._handleDocumentClick)
42
    document.body.addEventListener('mouseup', this._handleDocumentClick)
43
44
    Array.prototype.forEach.call(this._autocompletesRemove, (autocompleteRemove) => {
45
      autocompleteRemove.addEventListener('click', this._handleRemove)
46
    })
47
48
    Array.prototype.forEach.call(this._autocompletesRefresh, (autocompletesRefresh) => {
49
      autocompletesRefresh.removeEventListener('click', this._handleRefresh)
50
      autocompletesRefresh.addEventListener('click', this._handleRefresh)
51
    })
52
53
    Array.prototype.forEach.call(this._autocompletes, (autocomplete) => {
54
      document.body.removeEventListener('keydown', this._handleKeyDown)
55
      document.body.addEventListener('keydown', this._handleKeyDown)
56
      autocomplete.removeEventListener('keyup', this._handleKeyUp)
57
      autocomplete.addEventListener('keyup', this._handleKeyUp)
58
      autocomplete.removeEventListener('focus', this._handleFocus)
59
      autocomplete.addEventListener('focus', this._handleFocus)
60
      autocomplete.removeEventListener('blur', this._handleBlur)
61
      autocomplete.addEventListener('blur', this._handleBlur)
62
    })
63
  }
64
65
  _saveData() {
66
    var id = this._currentInput.getAttribute('data-id')
67
    var nodeComments = IframeCommentNode('#page-template', id.replace(/\./g, '-'))
68
    var maxLength = this._currentInput.getAttribute('data-maxlength')
69
70
    if(typeof maxLength !== 'undefined' && maxLength !== null && maxLength !== '') {
71
      maxLength = parseInt(maxLength)
72
      var countLength = [].slice.call(this._currentInput.parentNode.querySelectorAll('.autocomplete-result-wrapper .autocomplete-result')).length
73
      if(countLength === maxLength) {
74
        this._currentInput.value = ''
75
        this._divWrapper.parentNode.removeChild(this._divWrapper)
76
        this._currentInput.setAttribute('disabled', 'disabled')
77
      }else {
78
        this._currentInput.removeAttribute('disabled')
79
      }
80
    }
81
82
    var results = [].slice.call(this._currentInput.parentNode.querySelectorAll('.autocomplete-result-wrapper .autocomplete-result'))
83
    var json = this._json.data
84
    
85
    var toSave = []
86
    Array.prototype.forEach.call(results, (result) => {
87
      var value = result.getAttribute('value')
88
      if(value !== '') {
89
        if(value.indexOf('{') > -1 || value.indexOf('[') > -1) {
90
          toSave.push(JSON.parse(value))
91
        }else {
92
          toSave.push(value)
93
        }
94
      }
95
    })
96
    eval(`json.${id} = ${JSON.stringify(toSave)}`)
0 ignored issues
show
Security Performance introduced by
Calls to eval are slow and potentially dangerous, especially on untrusted code. Please consider whether there is another way to achieve your goal.
Loading history...
97
98
    this._json.data = json
99 View Code Duplication
    if(typeof nodeComments !== 'undefined' && nodeComments !== null && nodeComments.length > 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
100
      
101
      try {
102
        Array.prototype.forEach.call(nodeComments, (nodeComment) => {
103
          var blockHtml = unescape(nodeComment.textContent.replace(/\[\[([\S\s]*?)\]\]/, '')).replace(/\[0\]-/g, '[0]-')
104
105
          // var blockHtml = unescape(blockContent.innerHTML).replace(/\[0\]-/g, '[0]-')
106
          var template = Handlebars.compile(blockHtml, {noEscape: true})
107
          var compiled = template(this._json.data)
108
109
          nodeComment.parentNode.innerHTML = compiled + `<!-- ${nodeComment.textContent} -->`
110
        })
111
      } catch(e) {
112
        console.log(e)
0 ignored issues
show
Debugging Code introduced by
console.log looks like debug code. Are you sure you do not want to remove it?
Loading history...
113
      }
114
115
    }else if(typeof id !== 'undefined' && id !== null) {
116
      if (this._currentInput.getAttribute('visible') === true) {
117
        var nodes = EditorUtils.getNode(attr)
0 ignored issues
show
Bug introduced by
The variable attr seems to be never declared. If this is a global, consider adding a /** global: attr */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
118
        Array.prototype.forEach.call(nodes, (node) => {
119
          EditorUtils.formToHtml(node, this._currentInput)
120
        })
121
      }
122
    }
123
124
    this.onReload._fire()
125
  }
126
127
  _documentClick() {
128
    if(this._visible && !this._canSelect) {
129
      if(typeof this._divWrapper.parentNode !== 'undefined' && this._divWrapper.parentNode !== null) {
130
        this._hide()
131
      }
132
    }
133
  }
134
135
  _add(display, value, json, autocompleteResultWrapper) {
136
    var div = document.createElement('div')
137
    div.classList.add('autocomplete-result')
138
    div.setAttribute('data-parent-id', this._currentInput.getAttribute('data-id'))
139
    div.setAttribute('value', value.replace(/&quote;/g, '\''))
140
    div.innerHTML = this._prepareDisplay(json, display)
141
142
    var remove = document.createElement('span')
143
    remove.classList.add('glyphicon', 'glyphicon-remove')
144
    remove.setAttribute('data-autocomplete-remove', 'true')
145
    remove.addEventListener('click', this._handleRemove)
146
    div.appendChild(remove)
147
148
    autocompleteResultWrapper.appendChild(div)
149
  }
150
151
  _select(target) {
152
    var json = target.getAttribute('data-value').replace(/&quote;/g, '\'')
153
    if (json.indexOf('{') > -1) {
154
      json = JSON.parse(json)
155
    }
156
    var maxLength = this._currentInput.getAttribute('data-maxlength')
157
    if(typeof maxLength !== 'undefined' && maxLength !== null && maxLength !== '') {
158
      maxLength = parseInt(maxLength)
159
      var countLength = [].slice.call(this._currentInput.parentNode.querySelectorAll('.autocomplete-result-wrapper .autocomplete-result')).length
160
      if(countLength+1 > maxLength) {
161
        return
162
      }
163
    }
164
165
    this._add(
166
      this._currentInput.getAttribute('data-display'),
167
      target.getAttribute('data-value'),
168
      json,
169
      this._divWrapper.parentNode.querySelector('.autocomplete-result-wrapper')
170
    )
171
    this._saveData()
172
173
  }
174
175
  _selectValue(e) {
176
    this._select(e.currentTarget)
177
  }
178
179
  _showAutocomplete(obj, target, val) {
180
    var first = true
181
    var str = target.getAttribute('data-display')
182
    this._divWrapper.innerHTML = ''
183
    this.result = []
184
    var keys = this._getKeys(str)
185
    var key = keys[0]
186
    this._find(obj, key)
187
    Array.prototype.forEach.call(this.result, (o) => {
188
189
      var displayName = this._prepareDisplay(o, str, keys)
190
      if (displayName.toLowerCase().indexOf(val.toLowerCase()) > -1) {
191
        var div = document.createElement('div')
192
        div.addEventListener('mousedown', this._handleSelectValue)
193
        div.setAttribute('data-value', (typeof o == 'object') ? JSON.stringify(o) : o)
194
        div.setAttribute('data-display', displayName)
195
        if(first) {
196
          div.classList.add('selected')
197
        }
198
        first = false
199
        div.innerHTML = displayName.replace(new RegExp(`(${val})`, 'i'), '<span class="select">$1</span>')
200
        this._divWrapper.appendChild(div)
201
      }
202
    })
203
204
    this._show(target)
205
  }
206
207
  /**
208
   * add in array the object containing the object path if it exists in obj
209
   * @param  {Object}  obj  json object
210
   * @param  {string}  path the path to object (dot notation)
211
   */
212
  _find(obj, path) {
213
    if (path == null) {
214
      this.result = obj
215
    }else {
216
      if (this._has(obj, path)) {
217
        this.result.push(obj)
218
      }
219
      for (var key in obj) {
220
        if (obj.hasOwnProperty(key)) {
221
          if ('object' == typeof(obj[key]) && !this._has(obj[key], path)) {
222
            this._find(obj[key], path)
223
          } else if (this._has(obj[key], path)) {
224
            this.result.push(obj[key])
225
          }
226
        }
227
      }
228
    }
229
  }
230
231
  /**
232
   * return true if the object path exist in obj
233
   * @param  {Object}  obj  json object
234
   * @param  {string}  path the path to object (dot notation)
235
   * @return {Boolean}      is the path found in obj
236
   */
237
  _has(obj, path) {
238
    return path.split('.').every(function(x) {
239
      if(typeof obj != 'object' || obj === null || typeof obj[x] == 'undefined')
240
        return false
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
241
      obj = obj[x]
242
      return true
243
    })
244
  }
245
246
  /**
247
   * return the value from a json obj of a nested attribute
248
   * @param  {Object} obj  the json object
249
   * @param  {string} path the path to object (dot notation)
250
   * @return {[type]}      the object containing the path object or undefined
251
   */
252
  _get(obj, path) {
253
    return path.split('.').reduce(function(prev, curr) {
254
      return prev ? prev[curr] : undefined
255
    }, obj || this)
256
  }
257
258
  /**
259
   * replace the variables in str by values from obj
260
   * corresponding to keys
261
   * @param  {Object} obj    the json object
262
   * @param  {string} str    the string
263
   * @return {string}        the string with values
264
   */
265
  _prepareDisplay(obj, str = null) {
266
    var keys = this._getKeys(str)
267
    Array.prototype.forEach.call(keys, (key) => {
268
      var val = this._get(obj, key)
269
      var pattern = new RegExp('{{'+key+'}}|'+key, 'g')
270
      str = str.replace(pattern, val)
271
    })
272
273
    if (str == null) {
274
      str = obj
275
    }
276
277
    return str
278
  }
279
280
  /**
281
   * return array of variables {{variable}} extracted from str
282
   * @param  {string} str the string containing variables
283
   * @return {Array}     the array of variables
284
   */
285
  _getKeys(str){
286
    var regex = /\{\{(.*?)\}\}/g
287
    var variables = []
288
    var match
289
290
    while ((match = regex.exec(str)) !== null) {
291
      variables.push(match[1])
292
    }
293
    
294
    if (variables.length == 0 && str != null) {
295
      variables.push(str)
296
    }
297
298
    return variables
299
  }
300
301
  _hide() {
302
    if(this._visible) {
303
      this._visible = false
304
      this._shouldBeVisible = false
305
      if (this._divWrapper != null && this._divWrapper.parentNode) {
306
        this._divWrapper.parentNode.removeChild(this._divWrapper)
307
      }
308
    }
309
  }
310
311
  _show(target) {
312
    if(!this._visible) {
313
      this._visible = true
314
      this._divWrapper.style.marginTop = `${target.offsetHeight}px`
315
      this._divWrapper.style.width = `${target.offsetWidth}px`
316
      target.parentNode.insertBefore(this._divWrapper, target)
317
    }
318
  }
319
320
  _startAutocomplete(target) {
321
    var val = target.value.toLowerCase()
322
    if(val.length > 2) {
323
      if(this._previousValue === val) {
324
        this._show(target)
325
        return
326
      }else {
327
        this._previousValue = val
328
      }
329
      var dataVal = target.getAttribute('data-value').replace(/&quote;/g, '\'')
330
331
      if(dataVal.indexOf('{{') > -1){
332
        var match
333
        while (match = /\{\{(.*?)\}\}/.exec(dataVal)) {
334
          var selector = target.form.querySelector('[data-id="' + match[1] + '"]')
335
          if(selector != null) {
336
            dataVal = dataVal.replace('{{' + match[1] + '}}', selector.value)
337
          }
338
        }
339
      }
340
341
      if (dataVal.indexOf('http') === 0) {
342
        this._ajax(
343
          {
344
            url: `${dataVal}${val}`,
345
            body: '',
346
            cors: true,
347
            method: 'get'
348
          },
349
          (code, responseText) => {
350
            this._showAutocomplete(JSON.parse(responseText), target, val)
351
          })
352
      }else {
353
        var sources = JSON.parse(target.getAttribute('data-value').replace(/&quote;/g, '\''))
354
        this._showAutocomplete(sources, target, val)
355
      }
356
    }else {
357
      this._hide()
358
    }
359
  }
360
361
  _keyUp(e) {
362
    if(e.keyCode !== 13) {
363
      this._startAutocomplete(e.currentTarget)
364
    }
365
  }
366
367
  _refresh(e) {
368
    var target = e.currentTarget
369
    
370
    var autocompleteResultWrapper = target.parentNode.parentNode.querySelector('.autocomplete-result-wrapper')
371
    var autocompleteResult = autocompleteResultWrapper.querySelectorAll('.autocomplete-result')
372
    Array.prototype.forEach.call(autocompleteResult, (autocompleteResult) => {
373
      autocompleteResult.parentNode.removeChild(autocompleteResult)
374
    })
375
376
    var jsonPost = JSON.parse(JSON.stringify(json))
377
    delete jsonPost.abe_source
378
    this._currentInput = target.parentNode.parentNode.querySelector('input')
379
    var display = target.getAttribute('data-autocomplete-data-display')
380
    var body = qs.stringify({
381
      sourceString: target.getAttribute('data-autocomplete-refresh-sourcestring'),
382
      prefillQuantity: target.getAttribute('data-autocomplete-refresh-prefill-quantity'),
383
      key: target.getAttribute('data-autocomplete-refresh-key'),
384
      json: jsonPost
385
    })
386
387
    this._ajax(
388
      {
389
        url: '/abe/sql-request',
390
        body: body,
391
        cors: true,
392
        method: 'post'
393
      },
394
    (code, responseText) => {
395
      var items = JSON.parse(responseText)
396
      Array.prototype.forEach.call(items, function(item) {
397
        this._add(display, JSON.stringify(item), item, autocompleteResultWrapper)
398
      }.bind(this))
399
      this._saveData()
400
    })
401
  }
402
403
  _keyDown(e) {
404
    if(this._canSelect) {
405
      var parent = this._currentInput.parentNode.querySelector('.autocomplete-wrapper')
406
      if(typeof parent !== 'undefined' && parent !== null) {
407
        var current = this._currentInput.parentNode.querySelector('.autocomplete-wrapper .selected')
408
      
409
        var newSelected = null
410
        var selected = document.querySelector('.autocomplete-wrapper .selected')
411
        switch (e.keyCode) {
412
        case 9:
413
            // tab
414
          this._hide()
415
          break
416
        case 13:
417
            // enter
418
          e.preventDefault()
419
          if(typeof selected !== 'undefined' && selected !== null) {
420
            this._select(selected)
421
            this._hide()
422
          }
423
          break
424
        case 27:
425
            // escape
426
          e.preventDefault()
427
          this._hide()
428
          break
429
        case 40:
430
            // down
431
          e.preventDefault()
432
          if(typeof selected !== 'undefined' && selected !== null) {
433
            newSelected = selected.nextSibling
434
            this._show(e.currentTarget)
435
          }
436
          break
437
        case 38:
438
            // prev
439
          e.preventDefault()
440
          if(typeof selected !== 'undefined' && selected !== null) {
441
            newSelected = selected.previousSibling
442
          }
443
          break
444
        default:
445
          break
446
        }
447
448
        if(typeof newSelected !== 'undefined' && newSelected !== null) {
449
          var scrollTopMin = parent.scrollTop
450
          var scrollTopMax = parent.scrollTop + parent.offsetHeight - newSelected.offsetHeight
451
          var offsetTop = newSelected.offsetTop
452
          if (scrollTopMax < offsetTop) {
453
            parent.scrollTop = newSelected.offsetTop - parent.offsetHeight + newSelected.offsetHeight
454
          }else if (scrollTopMin > offsetTop) {
455
            parent.scrollTop = newSelected.offsetTop
456
          }
457
          current.classList.remove('selected')
458
          newSelected.classList.add('selected')
459
        }
460
      }
461
    }
462
  }
463
464
  _focus(e) {
465
    this._canSelect = true
466
    this._currentInput = e.currentTarget
467
    this._startAutocomplete(e.currentTarget)
468
  }
469
470
  _blur() {
471
    this._canSelect = false
472
    this._currentInput = null
473
    this._hide()
474
  }
475
476
  _remove(e) {
477
    var target = e.currentTarget.parentNode
478
    var escapedSelector = target.getAttribute('data-parent-id').replace(/(:|\.|\[|\])/g,'\\$1')
479
    this._currentInput = document.querySelector(`#${escapedSelector}`)
480
    target.parentNode.removeChild(target)
481
    this._saveData()
482
    this._currentInput = null
483
  }
484
}