Passed
Push — develop ( c0fc0b...96aa2b )
by Andrew
06:02
created

twigfield.ts ➔ makeMonacoEditor   F

Complexity

Conditions 23

Size

Total Lines 161
Code Lines 133

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 133
dl 0
loc 161
c 0
b 0
f 0
rs 0
cc 23

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 twigfield.ts ➔ makeMonacoEditor 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
/**
2
 * Twigfield Craft CMS
3
 *
4
 * Provides a twig editor field with Twig & Craft API autocomplete
5
 *
6
 * @link      https://nystudio107.com
7
 * @copyright Copyright (c) 2022 nystudio107
8
 */
9
10
/**
11
 * @author    nystudio107
12
 * @package   Twigfield
13
 * @since     1.0.0
14
 */
15
16
type MakeMonacoEditorFunction = (elementId: string, fieldType: string, wrapperClass: string, editorOptions: string, twigfieldOptions: string, endpointUrl: string, placeholderText: string) => void;
17
18
declare global {
19
  var __webpack_public_path__: string;
20
  var Craft: any;
21
  interface Window {
22
    twigfieldBaseAssetsUrl: string;
23
    makeMonacoEditor: MakeMonacoEditorFunction;
24
  }
25
}
26
27
// Set the __webpack_public_path__ dynamically so we can work inside of cpresources's hashed dir name
28
// https://stackoverflow.com/questions/39879680/example-of-setting-webpack-public-path-at-runtime
29
if (typeof __webpack_public_path__ !== 'string' || __webpack_public_path__ === '') {
30
  __webpack_public_path__ = window.twigfieldBaseAssetsUrl;
31
}
32
33
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
34
import {getCompletionItemsFromEndpoint} from './autocomplete';
35
import languageIcons from './language-icons'
36
37
// The default EditorOptions for the Monaco editor instance
38
// ref: https://microsoft.github.io/monaco-editor/api/enums/monaco.editor.EditorOption.html
39
const defaultOptions: monaco.editor.IStandaloneEditorConstructionOptions = {
40
  language: 'twig',
41
  theme: 'vs',
42
  automaticLayout: true,
43
  tabIndex: 0,
44
  // Disable sidebar line numbers
45
  lineNumbers: 'off',
46
  glyphMargin: false,
47
  folding: false,
48
  // Undocumented see https://github.com/Microsoft/vscode/issues/30795#issuecomment-410998882
49
  lineDecorationsWidth: 0,
50
  lineNumbersMinChars: 0,
51
  // Disable the current line highlight
52
  renderLineHighlight: 'none',
53
  wordWrap: 'on',
54
  scrollBeyondLastLine: false,
55
  scrollbar: {
56
    vertical: 'hidden',
57
    horizontal: 'auto',
58
    alwaysConsumeMouseWheel: false,
59
    handleMouseWheel: false,
60
  },
61
  fontSize: 14,
62
  fontFamily: 'SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace',
63
  minimap: {
64
    enabled: false
65
  },
66
};
67
68
// Create the editor
69
function makeMonacoEditor(elementId: string, fieldType: string, wrapperClass: string, editorOptions: string, twigfieldOptions: string, endpointUrl: string, placeholderText: string = '') {
70
  const textArea = <HTMLInputElement>document.getElementById(elementId);
71
  const container = document.createElement('div');
72
  const fieldOptions = JSON.parse(twigfieldOptions);
73
  const placeholderId = elementId + '-monaco-editor-placeholder';
74
  // If we can't find the passed in text area or if there is no parent node, return
75
  if (textArea === null || textArea.parentNode === null) {
76
    return;
77
  }
78
  // Monaco editor defaults, coalesced together
79
  const monacoEditorOptions: monaco.editor.IStandaloneEditorConstructionOptions = JSON.parse(editorOptions);
80
  let options: monaco.editor.IStandaloneEditorConstructionOptions = {...defaultOptions, ...monacoEditorOptions, ...{value: textArea.value}}
81
  // Make a sibling div for the Monaco editor to live in
82
  container.id = elementId + '-monaco-editor';
83
  container.classList.add('relative', 'box-content', 'monaco-editor-twigfield', 'h-full');
84
  // Add the icon in, if there is one
85
  const iconHtml = typeof options.language === "undefined" ? null : languageIcons[options.language];
86
  if (iconHtml) {
87
    const icon = document.createElement('div');
88
    icon.classList.add('monaco-editor-twigfield--icon');
89
    icon.setAttribute('title', Craft.t('twigfield', 'Twig code is supported.'));
90
    icon.setAttribute('aria-hidden', 'true');
91
    icon.innerHTML = iconHtml;
92
    container.appendChild(icon);
93
  }
94
  // Apply any passed in classes to the wrapper div
95
  if (wrapperClass !== '') {
96
    const cl = container.classList;
97
    const classArray = wrapperClass.trim().split(/\s+/);
98
    cl.add.apply(cl, classArray);
99
  }
100
  // Handle the placeholder text (if any)
101
  if (placeholderText !== '') {
102
    let placeholder = document.createElement('div');
103
    placeholder.id = elementId + '-monaco-editor-placeholder';
104
    placeholder.innerHTML = placeholderText;
105
    placeholder.classList.add('monaco-placeholder', 'p-2');
106
    container.appendChild(placeholder);
107
  }
108
  textArea.parentNode.insertBefore(container, textArea);
109
  textArea.style.display = 'none';
110
  // Create the Monaco editor
111
  let editor = monaco.editor.create(container, options);
112
  // When the text is changed in the editor, sync it to the underlying TextArea input
113
  editor.onDidChangeModelContent((event) => {
114
    textArea.value = editor.getValue();
115
  });
116
  // ref: https://github.com/vikyd/vue-monaco-singleline/blob/master/src/monaco-singleline.vue#L150
117
  if ('singleLineEditor' in fieldOptions && fieldOptions.singleLineEditor) {
118
    const textModel = editor.getModel();
119
    if (textModel !== null) {
120
      // Remove multiple spaces & tabs
121
      const text = textModel.getValue();
122
      textModel.setValue(text.replace(/\s\s+/g, ' '));
123
      // Handle the Find command
124
      editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyF, () => {
125
      });
126
      // Handle typing the Enter key
127
      editor.addCommand(monaco.KeyCode.Enter, () => {
128
      }, '!suggestWidgetVisible');
129
      // Handle typing the Tab key
130
      editor.addCommand(monaco.KeyCode.Tab, () => {
131
        focusNextElement();
132
      });
133
      editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Tab, () => {
134
        focusPrevElement();
135
      });
136
      // Handle Paste
137
      editor.onDidPaste((e) => {
138
        // multiple rows will be merged to single row
139
        let newContent = '';
140
        const lineCount = textModel.getLineCount();
141
        // remove all line breaks
142
        for (let i = 0; i < lineCount; i += 1) {
143
          newContent += textModel.getLineContent(i + 1);
144
        }
145
        // Remove multiple spaces & tabs
146
        newContent = newContent.replace(/\s\s+/g, ' ');
147
        textModel.setValue(newContent);
148
        editor.setPosition({column: newContent.length + 1, lineNumber: 1});
149
      })
150
    }
151
  }
152
  // Get the autocompletion items
153
  getCompletionItemsFromEndpoint(fieldType, twigfieldOptions, endpointUrl);
154
  // Custom resizer to always keep the editor full-height, without needing to scroll
155
  let ignoreEvent = false;
156
  const updateHeight = () => {
157
    const width = editor.getLayoutInfo().width;
158
    const contentHeight = Math.min(1000, editor.getContentHeight());
159
    //container.style.width = `${width}px`;
160
    container.style.height = `${contentHeight}px`;
161
    try {
162
      ignoreEvent = true;
163
      editor.layout({width, height: contentHeight});
164
    } finally {
165
      ignoreEvent = false;
166
    }
167
  };
168
  editor.onDidContentSizeChange(updateHeight);
169
  updateHeight();
170
  // Handle the placeholder
171
  if (placeholderText !== '') {
172
    showPlaceholder('#' + placeholderId, editor.getValue());
173
    editor.onDidBlurEditorWidget(() => {
174
      showPlaceholder('#' + placeholderId, editor.getValue());
175
    });
176
    editor.onDidFocusEditorWidget(() => {
177
      hidePlaceholder('#' + placeholderId);
178
    });
179
  }
180
181
  function focusNextElement() {
182
    var focussable = getFocusableElements();
183
    var index = focussable.indexOf(document.activeElement);
184
    if (index > -1) {
185
      var nextElement = focussable[index + 1] || focussable[0];
186
      nextElement.focus();
187
    }
188
  }
189
190
  function focusPrevElement() {
191
    var focussable = getFocusableElements();
192
    var index = focussable.indexOf(document.activeElement);
193
    if (index > -1) {
194
      var prevElement = focussable[index - 1] || focussable[focussable.length];
195
      prevElement.focus();
196
    }
197
  }
198
199
  function getFocusableElements() {
200
    var focussable = [];
201
    // add all elements we want to include in our selection
202
    const focussableElements = 'a:not([disabled]), button:not([disabled]), select:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])';
203
    const activeElement: HTMLFormElement = <HTMLFormElement>document.activeElement;
204
    if (activeElement && activeElement.form) {
205
      focussable = Array.prototype.filter.call(activeElement.form.querySelectorAll(focussableElements),
206
        function (element) {
207
          //check for visibility while always include the current activeElement
208
          return element.offsetWidth > 0 || element.offsetHeight > 0 || element === document.activeElement
209
        });
210
    }
211
212
    return focussable;
213
  }
214
215
  function showPlaceholder(selector: string, value: string) {
216
    if (value === "") {
217
      const elem = <HTMLElement>document.querySelector(selector);
218
      if (elem !== null) {
219
        elem.style.display = "initial";
220
      }
221
    }
222
  }
223
224
  function hidePlaceholder(selector: string) {
225
    const elem = <HTMLElement>document.querySelector(selector);
226
    if (elem !== null) {
227
      elem.style.display = "none";
228
    }
229
  }
230
}
231
232
window.makeMonacoEditor = makeMonacoEditor;
233
234
export default makeMonacoEditor;
235
236