/ src / text-editor-element.js
text-editor-element.js
  1  const { Emitter, Range } = require('atom');
  2  const Grim = require('grim');
  3  const TextEditorComponent = require('./text-editor-component');
  4  const dedent = require('dedent');
  5  
  6  class TextEditorElement extends HTMLElement {
  7    initialize(component) {
  8      this.component = component;
  9      return this;
 10    }
 11  
 12    get shadowRoot() {
 13      Grim.deprecate(dedent`
 14        The contents of \`atom-text-editor\` elements are no longer encapsulated
 15        within a shadow DOM boundary. Please, stop using \`shadowRoot\` and access
 16        the editor contents directly instead.
 17      `);
 18  
 19      return this;
 20    }
 21  
 22    get rootElement() {
 23      Grim.deprecate(dedent`
 24        The contents of \`atom-text-editor\` elements are no longer encapsulated
 25        within a shadow DOM boundary. Please, stop using \`rootElement\` and access
 26        the editor contents directly instead.
 27      `);
 28  
 29      return this;
 30    }
 31  
 32    createdCallback() {
 33      this.emitter = new Emitter();
 34      this.initialText = this.textContent;
 35      if (this.tabIndex == null) this.tabIndex = -1;
 36      this.addEventListener('focus', event =>
 37        this.getComponent().didFocus(event)
 38      );
 39      this.addEventListener('blur', event => this.getComponent().didBlur(event));
 40    }
 41  
 42    attachedCallback() {
 43      this.getComponent().didAttach();
 44      this.emitter.emit('did-attach');
 45    }
 46  
 47    detachedCallback() {
 48      this.emitter.emit('did-detach');
 49      this.getComponent().didDetach();
 50    }
 51  
 52    attributeChangedCallback(name, oldValue, newValue) {
 53      if (this.component) {
 54        switch (name) {
 55          case 'mini':
 56            this.getModel().update({ mini: newValue != null });
 57            break;
 58          case 'placeholder-text':
 59            this.getModel().update({ placeholderText: newValue });
 60            break;
 61          case 'gutter-hidden':
 62            this.getModel().update({ lineNumberGutterVisible: newValue == null });
 63            break;
 64          case 'readonly':
 65            this.getModel().update({ readOnly: newValue != null });
 66            break;
 67        }
 68      }
 69    }
 70  
 71    // Extended: Get a promise that resolves the next time the element's DOM
 72    // is updated in any way.
 73    //
 74    // This can be useful when you've made a change to the model and need to
 75    // be sure this change has been flushed to the DOM.
 76    //
 77    // Returns a {Promise}.
 78    getNextUpdatePromise() {
 79      return this.getComponent().getNextUpdatePromise();
 80    }
 81  
 82    getModel() {
 83      return this.getComponent().props.model;
 84    }
 85  
 86    setModel(model) {
 87      this.getComponent().update({ model });
 88      this.updateModelFromAttributes();
 89    }
 90  
 91    updateModelFromAttributes() {
 92      const props = { mini: this.hasAttribute('mini') };
 93      if (this.hasAttribute('placeholder-text'))
 94        props.placeholderText = this.getAttribute('placeholder-text');
 95      if (this.hasAttribute('gutter-hidden'))
 96        props.lineNumberGutterVisible = false;
 97  
 98      this.getModel().update(props);
 99      if (this.initialText) this.getModel().setText(this.initialText);
100    }
101  
102    onDidAttach(callback) {
103      return this.emitter.on('did-attach', callback);
104    }
105  
106    onDidDetach(callback) {
107      return this.emitter.on('did-detach', callback);
108    }
109  
110    measureDimensions() {
111      this.getComponent().measureDimensions();
112    }
113  
114    setWidth(width) {
115      this.style.width =
116        this.getComponent().getGutterContainerWidth() + width + 'px';
117    }
118  
119    getWidth() {
120      return this.getComponent().getScrollContainerWidth();
121    }
122  
123    setHeight(height) {
124      this.style.height = height + 'px';
125    }
126  
127    getHeight() {
128      return this.getComponent().getScrollContainerHeight();
129    }
130  
131    onDidChangeScrollLeft(callback) {
132      return this.emitter.on('did-change-scroll-left', callback);
133    }
134  
135    onDidChangeScrollTop(callback) {
136      return this.emitter.on('did-change-scroll-top', callback);
137    }
138  
139    // Deprecated: get the width of an `x` character displayed in this element.
140    //
141    // Returns a {Number} of pixels.
142    getDefaultCharacterWidth() {
143      return this.getComponent().getBaseCharacterWidth();
144    }
145  
146    // Extended: get the width of an `x` character displayed in this element.
147    //
148    // Returns a {Number} of pixels.
149    getBaseCharacterWidth() {
150      return this.getComponent().getBaseCharacterWidth();
151    }
152  
153    getMaxScrollTop() {
154      return this.getComponent().getMaxScrollTop();
155    }
156  
157    getScrollHeight() {
158      return this.getComponent().getScrollHeight();
159    }
160  
161    getScrollWidth() {
162      return this.getComponent().getScrollWidth();
163    }
164  
165    getVerticalScrollbarWidth() {
166      return this.getComponent().getVerticalScrollbarWidth();
167    }
168  
169    getHorizontalScrollbarHeight() {
170      return this.getComponent().getHorizontalScrollbarHeight();
171    }
172  
173    getScrollTop() {
174      return this.getComponent().getScrollTop();
175    }
176  
177    setScrollTop(scrollTop) {
178      const component = this.getComponent();
179      component.setScrollTop(scrollTop);
180      component.scheduleUpdate();
181    }
182  
183    getScrollBottom() {
184      return this.getComponent().getScrollBottom();
185    }
186  
187    setScrollBottom(scrollBottom) {
188      return this.getComponent().setScrollBottom(scrollBottom);
189    }
190  
191    getScrollLeft() {
192      return this.getComponent().getScrollLeft();
193    }
194  
195    setScrollLeft(scrollLeft) {
196      const component = this.getComponent();
197      component.setScrollLeft(scrollLeft);
198      component.scheduleUpdate();
199    }
200  
201    getScrollRight() {
202      return this.getComponent().getScrollRight();
203    }
204  
205    setScrollRight(scrollRight) {
206      return this.getComponent().setScrollRight(scrollRight);
207    }
208  
209    // Essential: Scrolls the editor to the top.
210    scrollToTop() {
211      this.setScrollTop(0);
212    }
213  
214    // Essential: Scrolls the editor to the bottom.
215    scrollToBottom() {
216      this.setScrollTop(Infinity);
217    }
218  
219    hasFocus() {
220      return this.getComponent().focused;
221    }
222  
223    // Extended: Converts a buffer position to a pixel position.
224    //
225    // * `bufferPosition` A {Point}-like object that represents a buffer position.
226    //
227    // Be aware that calling this method with a column that does not translate
228    // to column 0 on screen could cause a synchronous DOM update in order to
229    // measure the requested horizontal pixel position if it isn't already
230    // cached.
231    //
232    // Returns an {Object} with two values: `top` and `left`, representing the
233    // pixel position.
234    pixelPositionForBufferPosition(bufferPosition) {
235      const screenPosition = this.getModel().screenPositionForBufferPosition(
236        bufferPosition
237      );
238      return this.getComponent().pixelPositionForScreenPosition(screenPosition);
239    }
240  
241    // Extended: Converts a screen position to a pixel position.
242    //
243    // * `screenPosition` A {Point}-like object that represents a buffer position.
244    //
245    // Be aware that calling this method with a non-zero column value could
246    // cause a synchronous DOM update in order to measure the requested
247    // horizontal pixel position if it isn't already cached.
248    //
249    // Returns an {Object} with two values: `top` and `left`, representing the
250    // pixel position.
251    pixelPositionForScreenPosition(screenPosition) {
252      screenPosition = this.getModel().clipScreenPosition(screenPosition);
253      return this.getComponent().pixelPositionForScreenPosition(screenPosition);
254    }
255  
256    screenPositionForPixelPosition(pixelPosition) {
257      return this.getComponent().screenPositionForPixelPosition(pixelPosition);
258    }
259  
260    pixelRectForScreenRange(range) {
261      range = Range.fromObject(range);
262  
263      const start = this.pixelPositionForScreenPosition(range.start);
264      const end = this.pixelPositionForScreenPosition(range.end);
265      const lineHeight = this.getComponent().getLineHeight();
266  
267      return {
268        top: start.top,
269        left: start.left,
270        height: end.top + lineHeight - start.top,
271        width: end.left - start.left
272      };
273    }
274  
275    pixelRangeForScreenRange(range) {
276      range = Range.fromObject(range);
277      return {
278        start: this.pixelPositionForScreenPosition(range.start),
279        end: this.pixelPositionForScreenPosition(range.end)
280      };
281    }
282  
283    getComponent() {
284      if (!this.component) {
285        this.component = new TextEditorComponent({
286          element: this,
287          mini: this.hasAttribute('mini'),
288          updatedSynchronously: this.updatedSynchronously,
289          readOnly: this.hasAttribute('readonly')
290        });
291        this.updateModelFromAttributes();
292      }
293  
294      return this.component;
295    }
296  
297    setUpdatedSynchronously(updatedSynchronously) {
298      this.updatedSynchronously = updatedSynchronously;
299      if (this.component)
300        this.component.updatedSynchronously = updatedSynchronously;
301      return updatedSynchronously;
302    }
303  
304    isUpdatedSynchronously() {
305      return this.component
306        ? this.component.updatedSynchronously
307        : this.updatedSynchronously;
308    }
309  
310    // Experimental: Invalidate the passed block {Decoration}'s dimensions,
311    // forcing them to be recalculated and the surrounding content to be adjusted
312    // on the next animation frame.
313    //
314    // * {blockDecoration} A {Decoration} representing the block decoration you
315    // want to update the dimensions of.
316    invalidateBlockDecorationDimensions() {
317      this.getComponent().invalidateBlockDecorationDimensions(...arguments);
318    }
319  
320    setFirstVisibleScreenRow(row) {
321      this.getModel().setFirstVisibleScreenRow(row);
322    }
323  
324    getFirstVisibleScreenRow() {
325      return this.getModel().getFirstVisibleScreenRow();
326    }
327  
328    getLastVisibleScreenRow() {
329      return this.getModel().getLastVisibleScreenRow();
330    }
331  
332    getVisibleRowRange() {
333      return this.getModel().getVisibleRowRange();
334    }
335  
336    intersectsVisibleRowRange(startRow, endRow) {
337      return !(
338        endRow <= this.getFirstVisibleScreenRow() ||
339        this.getLastVisibleScreenRow() <= startRow
340      );
341    }
342  
343    selectionIntersectsVisibleRowRange(selection) {
344      const { start, end } = selection.getScreenRange();
345      return this.intersectsVisibleRowRange(start.row, end.row + 1);
346    }
347  
348    setFirstVisibleScreenColumn(column) {
349      return this.getModel().setFirstVisibleScreenColumn(column);
350    }
351  
352    getFirstVisibleScreenColumn() {
353      return this.getModel().getFirstVisibleScreenColumn();
354    }
355  }
356  
357  module.exports = document.registerElement('atom-text-editor', {
358    prototype: TextEditorElement.prototype
359  });