Passed
Push — master ( 7e07d9...30706c )
by Ron
02:08 queued 12s
created

plugin.js ➔ ... ➔ stepThroughMatches   F

Complexity

Conditions 22
Paths 169

Size

Total Lines 60
Code Lines 48

Duplication

Lines 56
Ratio 93.33 %

Importance

Changes 0
Metric Value
eloc 48
nc 169
nop 3
dl 56
loc 60
c 0
b 0
f 0
cc 22
rs 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like plugin.js ➔ ... ➔ stepThroughMatches often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
(function () {
2
var searchreplace = (function () {
0 ignored issues
show
Unused Code introduced by
The variable searchreplace seems to be never used. Consider removing it.
Loading history...
3
    'use strict';
4
5
    var Cell = function (initial) {
6
      var value = initial;
7
      var get = function () {
8
        return value;
9
      };
10
      var set = function (v) {
11
        value = v;
12
      };
13
      var clone = function () {
14
        return Cell(get());
15
      };
16
      return {
17
        get: get,
18
        set: set,
19
        clone: clone
20
      };
21
    };
22
23
    var global = tinymce.util.Tools.resolve('tinymce.PluginManager');
0 ignored issues
show
Bug introduced by
The variable tinymce seems to be never declared. If this is a global, consider adding a /** global: tinymce */ 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...
24
25
    var global$1 = tinymce.util.Tools.resolve('tinymce.util.Tools');
26
27
    function isContentEditableFalse(node) {
28
      return node && node.nodeType === 1 && node.contentEditable === 'false';
29
    }
30
    function findAndReplaceDOMText(regex, node, replacementNode, captureGroup, schema) {
31
      var m;
32
      var matches = [];
33
      var text, count = 0, doc;
34
      var blockElementsMap, hiddenTextElementsMap, shortEndedElementsMap;
35
      doc = node.ownerDocument;
36
      blockElementsMap = schema.getBlockElements();
37
      hiddenTextElementsMap = schema.getWhiteSpaceElements();
38
      shortEndedElementsMap = schema.getShortEndedElements();
39
      function getMatchIndexes(m, captureGroup) {
40
        captureGroup = captureGroup || 0;
41
        if (!m[0]) {
42
          throw new Error('findAndReplaceDOMText cannot handle zero-length matches');
43
        }
44
        var index = m.index;
45
        if (captureGroup > 0) {
46
          var cg = m[captureGroup];
47
          if (!cg) {
48
            throw new Error('Invalid capture group');
49
          }
50
          index += m[0].indexOf(cg);
51
          m[0] = cg;
52
        }
53
        return [
54
          index,
55
          index + m[0].length,
56
          [m[0]]
57
        ];
58
      }
59 View Code Duplication
      function getText(node) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
60
        var txt;
61
        if (node.nodeType === 3) {
62
          return node.data;
63
        }
64
        if (hiddenTextElementsMap[node.nodeName] && !blockElementsMap[node.nodeName]) {
65
          return '';
66
        }
67
        txt = '';
68
        if (isContentEditableFalse(node)) {
69
          return '\n';
70
        }
71
        if (blockElementsMap[node.nodeName] || shortEndedElementsMap[node.nodeName]) {
72
          txt += '\n';
73
        }
74
        if (node = node.firstChild) {
75
          do {
76
            txt += getText(node);
77
          } while (node = node.nextSibling);
78
        }
79
        return txt;
80
      }
81
      function stepThroughMatches(node, matches, replaceFn) {
82
        var startNode, endNode, startNodeIndex, endNodeIndex, innerNodes = [], atIndex = 0, curNode = node, matchLocation = matches.shift(), matchIndex = 0;
83
        out:
84 View Code Duplication
          while (true) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
85
            if (blockElementsMap[curNode.nodeName] || shortEndedElementsMap[curNode.nodeName] || isContentEditableFalse(curNode)) {
86
              atIndex++;
87
            }
88
            if (curNode.nodeType === 3) {
89
              if (!endNode && curNode.length + atIndex >= matchLocation[1]) {
90
                endNode = curNode;
91
                endNodeIndex = matchLocation[1] - atIndex;
92
              } else if (startNode) {
93
                innerNodes.push(curNode);
94
              }
95
              if (!startNode && curNode.length + atIndex > matchLocation[0]) {
96
                startNode = curNode;
97
                startNodeIndex = matchLocation[0] - atIndex;
98
              }
99
              atIndex += curNode.length;
100
            }
101
            if (startNode && endNode) {
102
              curNode = replaceFn({
103
                startNode: startNode,
104
                startNodeIndex: startNodeIndex,
0 ignored issues
show
Bug introduced by
The variable startNodeIndex seems to not be initialized for all possible execution paths.
Loading history...
105
                endNode: endNode,
106
                endNodeIndex: endNodeIndex,
0 ignored issues
show
Bug introduced by
The variable endNodeIndex seems to not be initialized for all possible execution paths.
Loading history...
107
                innerNodes: innerNodes,
108
                match: matchLocation[2],
109
                matchIndex: matchIndex
110
              });
111
              atIndex -= endNode.length - endNodeIndex;
112
              startNode = null;
113
              endNode = null;
114
              innerNodes = [];
115
              matchLocation = matches.shift();
116
              matchIndex++;
117
              if (!matchLocation) {
118
                break;
119
              }
120
            } else if ((!hiddenTextElementsMap[curNode.nodeName] || blockElementsMap[curNode.nodeName]) && curNode.firstChild) {
121
              if (!isContentEditableFalse(curNode)) {
122
                curNode = curNode.firstChild;
123
                continue;
124
              }
125
            } else if (curNode.nextSibling) {
126
              curNode = curNode.nextSibling;
127
              continue;
128
            }
129
            while (true) {
130
              if (curNode.nextSibling) {
131
                curNode = curNode.nextSibling;
132
                break;
133
              } else if (curNode.parentNode !== node) {
134
                curNode = curNode.parentNode;
135
              } else {
136
                break out;
137
              }
138
            }
139
          }
140
      }
141
      function genReplacer(nodeName) {
142
        var makeReplacementNode;
143
        if (typeof nodeName !== 'function') {
144
          var stencilNode_1 = nodeName.nodeType ? nodeName : doc.createElement(nodeName);
145
          makeReplacementNode = function (fill, matchIndex) {
146
            var clone = stencilNode_1.cloneNode(false);
147
            clone.setAttribute('data-mce-index', matchIndex);
148
            if (fill) {
149
              clone.appendChild(doc.createTextNode(fill));
150
            }
151
            return clone;
152
          };
153
        } else {
154
          makeReplacementNode = nodeName;
155
        }
156 View Code Duplication
        return function (range) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
157
          var before;
158
          var after;
159
          var parentNode;
160
          var startNode = range.startNode;
161
          var endNode = range.endNode;
162
          var matchIndex = range.matchIndex;
163
          if (startNode === endNode) {
164
            var node_1 = startNode;
165
            parentNode = node_1.parentNode;
166
            if (range.startNodeIndex > 0) {
167
              before = doc.createTextNode(node_1.data.substring(0, range.startNodeIndex));
168
              parentNode.insertBefore(before, node_1);
169
            }
170
            var el = makeReplacementNode(range.match[0], matchIndex);
171
            parentNode.insertBefore(el, node_1);
172
            if (range.endNodeIndex < node_1.length) {
173
              after = doc.createTextNode(node_1.data.substring(range.endNodeIndex));
174
              parentNode.insertBefore(after, node_1);
175
            }
176
            node_1.parentNode.removeChild(node_1);
177
            return el;
178
          }
179
          before = doc.createTextNode(startNode.data.substring(0, range.startNodeIndex));
180
          after = doc.createTextNode(endNode.data.substring(range.endNodeIndex));
181
          var elA = makeReplacementNode(startNode.data.substring(range.startNodeIndex), matchIndex);
182
          for (var i = 0, l = range.innerNodes.length; i < l; ++i) {
183
            var innerNode = range.innerNodes[i];
184
            var innerEl = makeReplacementNode(innerNode.data, matchIndex);
185
            innerNode.parentNode.replaceChild(innerEl, innerNode);
186
          }
187
          var elB = makeReplacementNode(endNode.data.substring(0, range.endNodeIndex), matchIndex);
188
          parentNode = startNode.parentNode;
189
          parentNode.insertBefore(before, startNode);
190
          parentNode.insertBefore(elA, startNode);
191
          parentNode.removeChild(startNode);
192
          parentNode = endNode.parentNode;
193
          parentNode.insertBefore(elB, endNode);
194
          parentNode.insertBefore(after, endNode);
195
          parentNode.removeChild(endNode);
196
          return elB;
197
        };
198
      }
199
      text = getText(node);
200
      if (!text) {
201
        return;
0 ignored issues
show
Comprehensibility Best Practice introduced by
Are you sure this return statement is not missing an argument? If this is intended, consider adding an explicit undefined like return undefined;.
Loading history...
202
      }
203
      if (regex.global) {
204
        while (m = regex.exec(text)) {
205
          matches.push(getMatchIndexes(m, captureGroup));
206
        }
207
      } else {
208
        m = text.match(regex);
209
        matches.push(getMatchIndexes(m, captureGroup));
210
      }
211
      if (matches.length) {
212
        count = matches.length;
213
        stepThroughMatches(node, matches, genReplacer(replacementNode));
214
      }
215
      return count;
216
    }
217
    var FindReplaceText = { findAndReplaceDOMText: findAndReplaceDOMText };
218
219
    var getElmIndex = function (elm) {
220
      var value = elm.getAttribute('data-mce-index');
221
      if (typeof value === 'number') {
222
        return '' + value;
223
      }
224
      return value;
225
    };
226
    var markAllMatches = function (editor, currentIndexState, regex) {
227
      var node, marker;
228
      marker = editor.dom.create('span', { 'data-mce-bogus': 1 });
229
      marker.className = 'mce-match-marker';
230
      node = editor.getBody();
231
      done(editor, currentIndexState, false);
232
      return FindReplaceText.findAndReplaceDOMText(regex, node, marker, false, editor.schema);
233
    };
234
    var unwrap = function (node) {
235
      var parentNode = node.parentNode;
236
      if (node.firstChild) {
237
        parentNode.insertBefore(node.firstChild, node);
238
      }
239
      node.parentNode.removeChild(node);
240
    };
241 View Code Duplication
    var findSpansByIndex = function (editor, index) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
242
      var nodes;
243
      var spans = [];
244
      nodes = global$1.toArray(editor.getBody().getElementsByTagName('span'));
245
      if (nodes.length) {
246
        for (var i = 0; i < nodes.length; i++) {
247
          var nodeIndex = getElmIndex(nodes[i]);
248
          if (nodeIndex === null || !nodeIndex.length) {
249
            continue;
250
          }
251
          if (nodeIndex === index.toString()) {
252
            spans.push(nodes[i]);
253
          }
254
        }
255
      }
256
      return spans;
257
    };
258
    var moveSelection = function (editor, currentIndexState, forward) {
259
      var testIndex = currentIndexState.get();
260
      var dom = editor.dom;
261
      forward = forward !== false;
262
      if (forward) {
263
        testIndex++;
264
      } else {
265
        testIndex--;
266
      }
267
      dom.removeClass(findSpansByIndex(editor, currentIndexState.get()), 'mce-match-marker-selected');
268
      var spans = findSpansByIndex(editor, testIndex);
269
      if (spans.length) {
270
        dom.addClass(findSpansByIndex(editor, testIndex), 'mce-match-marker-selected');
271
        editor.selection.scrollIntoView(spans[0]);
272
        return testIndex;
273
      }
274
      return -1;
275
    };
276
    var removeNode = function (dom, node) {
277
      var parent = node.parentNode;
278
      dom.remove(node);
279
      if (dom.isEmpty(parent)) {
280
        dom.remove(parent);
281
      }
282
    };
283
    var find = function (editor, currentIndexState, text, matchCase, wholeWord) {
284
      text = text.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
285
      text = text.replace(/\s/g, '[^\\S\\r\\n]');
286
      text = wholeWord ? '\\b' + text + '\\b' : text;
287
      var count = markAllMatches(editor, currentIndexState, new RegExp(text, matchCase ? 'g' : 'gi'));
288
      if (count) {
289
        currentIndexState.set(-1);
290
        currentIndexState.set(moveSelection(editor, currentIndexState, true));
291
      }
292
      return count;
293
    };
294
    var next = function (editor, currentIndexState) {
295
      var index = moveSelection(editor, currentIndexState, true);
296
      if (index !== -1) {
297
        currentIndexState.set(index);
298
      }
299
    };
300
    var prev = function (editor, currentIndexState) {
301
      var index = moveSelection(editor, currentIndexState, false);
302
      if (index !== -1) {
303
        currentIndexState.set(index);
304
      }
305
    };
306
    var isMatchSpan = function (node) {
307
      var matchIndex = getElmIndex(node);
308
      return matchIndex !== null && matchIndex.length > 0;
309
    };
310
    var replace = function (editor, currentIndexState, text, forward, all) {
311
      var i, nodes, node, matchIndex, currentMatchIndex, nextIndex = currentIndexState.get(), hasMore;
312
      forward = forward !== false;
313
      node = editor.getBody();
314
      nodes = global$1.grep(global$1.toArray(node.getElementsByTagName('span')), isMatchSpan);
315
      for (i = 0; i < nodes.length; i++) {
316
        var nodeIndex = getElmIndex(nodes[i]);
317
        matchIndex = currentMatchIndex = parseInt(nodeIndex, 10);
318
        if (all || matchIndex === currentIndexState.get()) {
319
          if (text.length) {
320
            nodes[i].firstChild.nodeValue = text;
321
            unwrap(nodes[i]);
322
          } else {
323
            removeNode(editor.dom, nodes[i]);
324
          }
325
          while (nodes[++i]) {
0 ignored issues
show
Complexity Coding Style introduced by
You seem to be assigning a new value to the loop variable i here. Please check if this was indeed your intention. Even if it was, consider using another kind of loop instead.
Loading history...
326
            matchIndex = parseInt(getElmIndex(nodes[i]), 10);
327
            if (matchIndex === currentMatchIndex) {
328
              removeNode(editor.dom, nodes[i]);
329
            } else {
330
              i--;
331
              break;
332
            }
333
          }
334
          if (forward) {
335
            nextIndex--;
336
          }
337
        } else if (currentMatchIndex > currentIndexState.get()) {
338
          nodes[i].setAttribute('data-mce-index', currentMatchIndex - 1);
339
        }
340
      }
341
      currentIndexState.set(nextIndex);
342
      if (forward) {
343
        hasMore = hasNext(editor, currentIndexState);
344
        next(editor, currentIndexState);
345
      } else {
346
        hasMore = hasPrev(editor, currentIndexState);
347
        prev(editor, currentIndexState);
348
      }
349
      return !all && hasMore;
350
    };
351
    var done = function (editor, currentIndexState, keepEditorSelection) {
352
      var i, nodes, startContainer, endContainer;
353
      nodes = global$1.toArray(editor.getBody().getElementsByTagName('span'));
354
      for (i = 0; i < nodes.length; i++) {
355
        var nodeIndex = getElmIndex(nodes[i]);
356
        if (nodeIndex !== null && nodeIndex.length) {
357
          if (nodeIndex === currentIndexState.get().toString()) {
358
            if (!startContainer) {
359
              startContainer = nodes[i].firstChild;
360
            }
361
            endContainer = nodes[i].firstChild;
362
          }
363
          unwrap(nodes[i]);
364
        }
365
      }
366
      if (startContainer && endContainer) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if startContainer && endContainer is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
367
        var rng = editor.dom.createRng();
368
        rng.setStart(startContainer, 0);
369
        rng.setEnd(endContainer, endContainer.data.length);
370
        if (keepEditorSelection !== false) {
371
          editor.selection.setRng(rng);
372
        }
373
        return rng;
374
      }
375
    };
376
    var hasNext = function (editor, currentIndexState) {
377
      return findSpansByIndex(editor, currentIndexState.get() + 1).length > 0;
378
    };
379
    var hasPrev = function (editor, currentIndexState) {
380
      return findSpansByIndex(editor, currentIndexState.get() - 1).length > 0;
381
    };
382
    var Actions = {
383
      done: done,
384
      find: find,
385
      next: next,
386
      prev: prev,
387
      replace: replace,
388
      hasNext: hasNext,
389
      hasPrev: hasPrev
390
    };
391
392
    var get = function (editor, currentIndexState) {
393
      var done = function (keepEditorSelection) {
394
        return Actions.done(editor, currentIndexState, keepEditorSelection);
395
      };
396
      var find = function (text, matchCase, wholeWord) {
397
        return Actions.find(editor, currentIndexState, text, matchCase, wholeWord);
398
      };
399
      var next = function () {
400
        return Actions.next(editor, currentIndexState);
401
      };
402
      var prev = function () {
403
        return Actions.prev(editor, currentIndexState);
404
      };
405
      var replace = function (text, forward, all) {
406
        return Actions.replace(editor, currentIndexState, text, forward, all);
407
      };
408
      return {
409
        done: done,
410
        find: find,
411
        next: next,
412
        prev: prev,
413
        replace: replace
414
      };
415
    };
416
    var Api = { get: get };
417
418
    var open = function (editor, currentIndexState) {
419
      var last = {}, selectedText;
420
      editor.undoManager.add();
421
      selectedText = global$1.trim(editor.selection.getContent({ format: 'text' }));
422
      function updateButtonStates() {
423
        win.statusbar.find('#next').disabled(Actions.hasNext(editor, currentIndexState) === false);
424
        win.statusbar.find('#prev').disabled(Actions.hasPrev(editor, currentIndexState) === false);
425
      }
426
      function notFoundAlert() {
427
        editor.windowManager.alert('Could not find the specified string.', function () {
428
          win.find('#find')[0].focus();
429
        });
430
      }
431
      var win = editor.windowManager.open({
432
        layout: 'flex',
433
        pack: 'center',
434
        align: 'center',
435
        onClose: function () {
436
          editor.focus();
437
          Actions.done(editor, currentIndexState);
438
          editor.undoManager.add();
439
        },
440
        onSubmit: function (e) {
441
          var count, caseState, text, wholeWord;
442
          e.preventDefault();
443
          caseState = win.find('#case').checked();
444
          wholeWord = win.find('#words').checked();
445
          text = win.find('#find').value();
446
          if (!text.length) {
447
            Actions.done(editor, currentIndexState, false);
448
            win.statusbar.items().slice(1).disabled(true);
449
            return;
450
          }
451
          if (last.text === text && last.caseState === caseState && last.wholeWord === wholeWord) {
452
            if (!Actions.hasNext(editor, currentIndexState)) {
453
              notFoundAlert();
454
              return;
455
            }
456
            Actions.next(editor, currentIndexState);
457
            updateButtonStates();
458
            return;
459
          }
460
          count = Actions.find(editor, currentIndexState, text, caseState, wholeWord);
461
          if (!count) {
462
            notFoundAlert();
463
          }
464
          win.statusbar.items().slice(1).disabled(count === 0);
465
          updateButtonStates();
466
          last = {
467
            text: text,
468
            caseState: caseState,
469
            wholeWord: wholeWord
470
          };
471
        },
472
        buttons: [
473
          {
474
            text: 'Find',
475
            subtype: 'primary',
476
            onclick: function () {
477
              win.submit();
478
            }
479
          },
480
          {
481
            text: 'Replace',
482
            disabled: true,
483
            onclick: function () {
484
              if (!Actions.replace(editor, currentIndexState, win.find('#replace').value())) {
485
                win.statusbar.items().slice(1).disabled(true);
486
                currentIndexState.set(-1);
487
                last = {};
488
              }
489
            }
490
          },
491
          {
492
            text: 'Replace all',
493
            disabled: true,
494
            onclick: function () {
495
              Actions.replace(editor, currentIndexState, win.find('#replace').value(), true, true);
496
              win.statusbar.items().slice(1).disabled(true);
497
              last = {};
498
            }
499
          },
500
          {
501
            type: 'spacer',
502
            flex: 1
503
          },
504
          {
505
            text: 'Prev',
506
            name: 'prev',
507
            disabled: true,
508
            onclick: function () {
509
              Actions.prev(editor, currentIndexState);
510
              updateButtonStates();
511
            }
512
          },
513
          {
514
            text: 'Next',
515
            name: 'next',
516
            disabled: true,
517
            onclick: function () {
518
              Actions.next(editor, currentIndexState);
519
              updateButtonStates();
520
            }
521
          }
522
        ],
523
        title: 'Find and replace',
524
        items: {
525
          type: 'form',
526
          padding: 20,
527
          labelGap: 30,
528
          spacing: 10,
529
          items: [
530
            {
531
              type: 'textbox',
532
              name: 'find',
533
              size: 40,
534
              label: 'Find',
535
              value: selectedText
536
            },
537
            {
538
              type: 'textbox',
539
              name: 'replace',
540
              size: 40,
541
              label: 'Replace with'
542
            },
543
            {
544
              type: 'checkbox',
545
              name: 'case',
546
              text: 'Match case',
547
              label: ' '
548
            },
549
            {
550
              type: 'checkbox',
551
              name: 'words',
552
              text: 'Whole words',
553
              label: ' '
554
            }
555
          ]
556
        }
557
      });
558
    };
559
    var Dialog = { open: open };
560
561
    var register = function (editor, currentIndexState) {
562
      editor.addCommand('SearchReplace', function () {
563
        Dialog.open(editor, currentIndexState);
564
      });
565
    };
566
    var Commands = { register: register };
567
568
    var showDialog = function (editor, currentIndexState) {
569
      return function () {
570
        Dialog.open(editor, currentIndexState);
571
      };
572
    };
573
    var register$1 = function (editor, currentIndexState) {
574
      editor.addMenuItem('searchreplace', {
575
        text: 'Find and replace',
576
        shortcut: 'Meta+F',
577
        onclick: showDialog(editor, currentIndexState),
578
        separator: 'before',
579
        context: 'edit'
580
      });
581
      editor.addButton('searchreplace', {
582
        tooltip: 'Find and replace',
583
        onclick: showDialog(editor, currentIndexState)
584
      });
585
      editor.shortcuts.add('Meta+F', '', showDialog(editor, currentIndexState));
586
    };
587
    var Buttons = { register: register$1 };
588
589
    global.add('searchreplace', function (editor) {
590
      var currentIndexState = Cell(-1);
591
      Commands.register(editor, currentIndexState);
592
      Buttons.register(editor, currentIndexState);
593
      return Api.get(editor, currentIndexState);
594
    });
595
    function Plugin () {
596
    }
597
598
    return Plugin;
599
600
}());
601
})();
602