text: import Component from '@ember/component';
  import getScrollParent from '../utils/get-scroll-parent';
  import relativeToAbsolute from '../lib/relative-to-absolute';
  import {TOOLBAR_MARGIN} from './koenig-toolbar';
  import {computed} from '@ember/object';
  import {getLinkMarkupFromRange} from '../utils/markup-utils';
  import {htmlSafe} from '@ember/string';
  import {run} from '@ember/runloop';
  import {inject as service} from '@ember/service';
  
  // pixels that should be added to the `left` property of the tick adjustment styles
  // TODO: handle via CSS?
  const TICK_ADJUSTMENT = 8;
  
  export default Component.extend({
      config: service(),
  
      attributeBindings: ['style'],
      classNames: ['kg-input-bar', 'absolute', 'z-999'],
  
      // public attrs
      editor: null,
      linkRange: null,
      linkRect: null,
      selectedRange: null,
  
      // internal properties
      top: null,
      left: null,
      right: null,
      href: '',
  
      // private properties
      _selectedRange: null,
      _windowRange: null,
      _onMousedownHandler: null,
      _onMouseupHandler: null,
  
      // closure actions
      cancel() {},
  
      /* computed properties -------------------------------------------------- */
  
      style: computed('top', 'left', 'right', function () {
          let position = this.getProperties('top', 'left', 'right');
          let styles = Object.keys(position).map((style) => {
              if (position[style] !== null) {
                  return `${style}: ${position[style]}px`;
              }
          });
  
          return htmlSafe(styles.compact().join('; '));
      }),
  
      /* lifecycle hooks ------------------------------------------------------ */
  
      init() {
          this._super(...arguments);
  
          // record the range now because the property is bound and will update
          // as we make changes whilst calculating the link position
          this._selectedRange = this.selectedRange;
          this._linkRange = this.linkRange;
  
          // grab a window range so that we can use getBoundingClientRect. Using
          // document.createRange is more efficient than doing editor.setRange
          // because it doesn't trigger all of the selection changing side-effects
          // TODO: extract MobiledocRange->NativeRange into a util
          let editor = this.editor;
          let cursor = editor.cursor;
          let {head, tail} = this._linkRange;
          let {node: headNode, offset: headOffset} = cursor._findNodeForPosition(head);
          let {node: tailNode, offset: tailOffset} = cursor._findNodeForPosition(tail);
          let range = document.createRange();
          range.setStart(headNode, headOffset);
          range.setEnd(tailNode, tailOffset);
          this._windowRange = range;
  
          // wait until rendered to position so that we have access to this.element
          run.schedule('afterRender', this, function () {
              this._positionToolbar();
              this._focusInput();
          });
  
          // grab an existing href value if there is one
          this._getHrefFromMarkup();
  
          // watch the window for mousedown events so that we can close the menu
          // when we detect a click outside
          this._onMousedownHandler = run.bind(this, this._handleMousedown);
          window.addEventListener('mousedown', this._onMousedownHandler);
  
          // watch for keydown events so that we can close the menu on Escape
          this._onKeydownHandler = run.bind(this, this._handleKeydown);
          window.addEventListener('keydown', this._onKeydownHandler);
      },
  
      willDestroyElement() {
          this._super(...arguments);
          this._removeStyleElement();
          window.removeEventListener('mousedown', this._onMousedownHandler);
          window.removeEventListener('keydown', this._onKeydownHandler);
      },
  
      actions: {
          inputKeydown(event) {
              if (event.key === 'Enter') {
                  // prevent Enter from triggering in the editor and removing text
                  event.preventDefault();
  
                  let href = relativeToAbsolute(this.href, this.config.get('blogUrl'));
                  this.set('href', href);
  
                  // create a single editor runloop here so that we don't get
                  // separate remove and replace ops pushed onto the undo stack
                  this.editor.run((postEditor) => {
                      if (href) {
                          this._replaceLink(href, postEditor);
                      } else {
                          this._removeLinks(postEditor);
                      }
                  });
  
                  this._cancelAndReselect();
              }
          },
  
          clear() {
              this.set('href', '');
              this._focusInput();
          }
      },
  
      // if we have a single link or a slice of a single link selected, grab the
      // href and adjust our linkRange to encompass the whole link
      _getHrefFromMarkup() {
          let linkMarkup = getLinkMarkupFromRange(this._linkRange);
          if (linkMarkup) {
              this.set('href', linkMarkup.attributes.href);
              this._linkRange = this._linkRange.expandByMarker(marker => !!marker.markups.includes(linkMarkup));
          }
      },
  
      _replaceLink(href, postEditor) {
          this._removeLinks(postEditor);
          let linkMarkup = postEditor.builder.createMarkup('a', {href});
          postEditor.toggleMarkup(linkMarkup, this._linkRange);
      },
  
      // loop over all markers that are touched by linkRange, removing any 'a'
      // markups on them to clear all links
      _removeLinks(postEditor) {
          let {headMarker, tailMarker} = this.linkRange;
          let curMarker = headMarker;
  
          while (curMarker && curMarker !== tailMarker.next) {
              curMarker.markups.filterBy('tagName', 'a').forEach((markup) => {
                  curMarker.removeMarkup(markup);
                  postEditor._markDirty(curMarker);
              });
              curMarker = curMarker.next;
          }
      },
  
      _cancelAndReselect() {
          this.cancel();
          if (this._selectedRange) {
              this.editor.selectRange(this._selectedRange);
          }
      },
  
      _focusInput() {
          let scrollParent = getScrollParent(this.element);
          let scrollTop = scrollParent.scrollTop;
  
          this.element.querySelector('input').focus();
  
          // reset the scroll position to avoid jumps
          // TODO: why does the input focus cause a scroll to the bottom of the doc?
          scrollParent.scrollTop = scrollTop;
      },
  
      // TODO: largely shared with {{koenig-toolbar}} and {{koenig-snippet-input}} - extract to a shared util?
      _positionToolbar() {
          let containerRect = this.element.offsetParent.getBoundingClientRect();
          let rangeRect = this.linkRect || this._windowRange.getBoundingClientRect();
          let {width, height} = this.element.getBoundingClientRect();
          let newPosition = {};
  
          // rangeRect is relative to the viewport so we need to subtract the
          // container measurements to get a position relative to the container
          newPosition = {
              top: rangeRect.top - containerRect.top - height - TOOLBAR_MARGIN,
              left: rangeRect.left - containerRect.left + rangeRect.width / 2 - width / 2,
              right: null
          };
  
          let tickPosition = 50;
          // don't overflow left boundary
          if (newPosition.left < 0) {
              newPosition.left = 0;
  
              // calculate the tick percentage position
              let absTickPosition = rangeRect.left - containerRect.left + rangeRect.width / 2;
              tickPosition = absTickPosition / width * 100;
              if (tickPosition < 5) {
                  tickPosition = 5;
              }
          }
          // same for right boundary
          if (newPosition.left + width > containerRect.width) {
              newPosition.left = null;
              newPosition.right = 0;
  
              // calculate the tick percentage position
              let absTickPosition = rangeRect.right - containerRect.right - rangeRect.width / 2;
              tickPosition = 100 + absTickPosition / width * 100;
              if (tickPosition > 95) {
                  tickPosition = 95;
              }
          }
  
          // the tick is a pseudo-element so we the only way we can affect it's
          // style is by adding a style element to the head
          this._removeStyleElement(); // reset to base styles
          if (tickPosition !== 50) {
              this._addStyleElement(`left: calc(${tickPosition}% - ${TICK_ADJUSTMENT}px)`);
          }
  
          // update the toolbar position
          this.setProperties(newPosition);
      },
  
      _addStyleElement(styles) {
          let styleElement = document.createElement('style');
          styleElement.id = `${this.elementId}-style`;
          styleElement.innerHTML = `#${this.elementId}:before, #${this.elementId}:after { ${styles} }`;
          document.head.appendChild(styleElement);
      },
  
      _removeStyleElement() {
          let styleElement = document.querySelector(`#${this.elementId}-style`);
          if (styleElement) {
              styleElement.remove();
          }
      },
  
      _handleMousedown(event) {
          if (!event.target.closest(`#${this.elementId}`)) {
              // no need to re-select for mouse clicks
              this.cancel();
          }
      },
  
      _handleKeydown(event) {
          if (event.key === 'Escape') {
              this._cancelAndReselect();
          }
      }
  });
  