Jump to content

User:Þjarkur/IPA popups.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/*

	Loads IPA data from the help page and displays as a tooltip.

	Popup functionality from [[MediaWiki:Gadget-ReferenceTooltips.js]], requires that the gadget be turned on in settings for the CSS to work.

*/
function GetIPATable(element, callback) {
  /* Will save data on the form { "IPA letter": "HTML table row"} */
  var IPA_keys = {}
  var IPA
  var help_page_url
  var help_page_title
  var header = null

  function find_rows(html) {
    $(html).find('.wikitable tr').each(function () {
      $(this).find('sup').remove()
      if ($(this).children('th').length > 0 && $(this).children('th[colspan]').length === 0) {
        if(header) return;
        header = $(this).html()
      } else if ($(this).children('td').length > 0) {
      	$(this).find('[rowspan], [colspan]').attr('rowspan','').attr('colspan','')
        var IPA_key = $(this).children('td').first().text().replace(/[◌]/g, '').trim()
        IPA_keys[IPA_key] = $(this).html()
      }
    })
    create_table()
    console.log(Object.keys(IPA_keys))
  }

  function create_table() {
    var array_of_IPA_keys = Object.keys(IPA_keys).sort((a, b) => b.length - a.length)
    var r = new RegExp('( |' + array_of_IPA_keys.map(escapeRegExp).join('|') + ')')
    var split = IPA.split(r).filter(Boolean)
    var table = `
      Information from <a href="${help_page_url}">${help_page_title}</a>
      <table class="wikitable">
        <tr>${header}</tr>
        ${split.map(key => {
          if(key ==' ') {
            return '<tr><td colspan=10></td></tr>'
          }
          return `<tr>${IPA_keys[key] || `<td colspan=10>Missing data for "<b>${key}</b>"</td>`}</tr>`
        }).join('')}
      </table>
    `
    callback(table)
  }

  // Taken from developer.mozilla.org
  function escapeRegExp(string) {
    return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
  }

	/*
		Run
	*/
  IPA = $(element).text().replace(/[\[\]/]/g, '').trim()
  help_page_url = $(element).attr('href')
  help_page_title = help_page_url.replace('/wiki/', '')
  if (!help_page_title.startsWith('Help:IPA/')) return;
  $.ajax({
    url: help_page_url,
    type: 'get',
    success: function (html) {
      find_rows(html)
    }
  })
}









// Derived from [[MediaWiki:Gadget-ReferenceTooltips.js]]
(function () {
  var REF_LINK_SELECTOR = '.IPA a'

  mw.messages.set({
    'rt-settings': 'Reference Tooltips settings',
    'rt-enable-footer': 'Enable Reference Tooltips',
    'rt-settings-title': 'Reference Tooltips',
    'rt-save': 'Save',
    'rt-cancel': 'Cancel',
    'rt-enable': 'Enable',
    'rt-disable': 'Disable',
    'rt-activationMethod': 'Tooltip appears when',
    'rt-hovering': 'hovering',
    'rt-clicking': 'clicking',
    'rt-delay': 'Delay before the tooltip appears (in milliseconds)',
    'rt-disabledNote': 'You can re-enable Reference Tooltips using a link in the footer of the page.',
    'rt-done': 'Done',
    'rt-enabled': 'Reference Tooltips are enabled'
  });

  // "Global" variables
  var SECONDS_IN_A_DAY = 60 * 60 * 24,
    CLASSES = {
      FADE_IN_DOWN: 'rt-fade-in-down',
      FADE_IN_UP: 'rt-fade-in-up',
      FADE_OUT_DOWN: 'rt-fade-out-down',
      FADE_OUT_UP: 'rt-fade-out-up'
    },
    IS_TOUCHSCREEN = 'ontouchstart' in document.documentElement,
    // Quite a rough check for mobile browsers, a mix of what is advised at
    // https://stackoverflow.com/a/24600597 (sends to
    // https://developer.mozilla.org/en-US/docs/Browser_detection_using_the_user_agent)
    // and https://stackoverflow.com/a/14301832
    IS_MOBILE = /Mobi|Android/i.test(navigator.userAgent) ||
    typeof window.orientation !== 'undefined',
    CLIENT_NAME = $.client.profile().name,
    settingsString, settings, enabled, delay, activatedByClick, tooltipsForComments, cursorWaitCss,
    windowManager,
    $body = $(document.body),
    $window = $(window);

  function rt($content) {
    // Popups gadget
    if (window.pg) {
      return;
    }

    var teSelector

    function TooltippedElement($element) {
      var tooltip,
        events,
        thisElement = this;

      function onStartEvent(e) {

        var showRefArgs;

        if (activatedByClick) {
          e.preventDefault();
        }
        if (!thisElement.noRef) {
          showRefArgs = [$(this)];
          // showRefArgs.push(e.pageX, e.pageY);
					thisElement.showRef.apply( thisElement, showRefArgs );
        }
      }

      function onEndEvent() {
        if (!thisElement.noRef) {
          thisElement.hideRef();
        }
      }

      if (!$element) {
        return;
      }

      // TooltippedElement.$element and TooltippedElement.$originalElement will be different when
      // the first is changed after its cloned version is hovered in a tooltip
      this.$element = $element;
      this.$originalElement = $element;

      if (activatedByClick) {
        events = {
          'click.rt': onStartEvent
        };
        // Adds an ability to see tooltips for links
        if (this.type === 'commentedText' &&
          (this.$element.closest('a').length ||
            this.$element.has('a').length
          )
        ) {
          events['contextmenu.rt'] = onStartEvent;
        }
      } else {
        events = {
          'mouseenter.rt': onStartEvent,
          'mouseleave.rt': onEndEvent
        };
      }

      this.$element.on(events);

      this.hideRef = function (immediately) {
        clearTimeout(thisElement.showTimer);

        if (this.type === 'commentedText') {
          this.$element.attr('title', this.comment);
        }

        if (this.tooltip && this.tooltip.isPresent) {
          if (activatedByClick || immediately) {
            this.tooltip.hide();
          } else {
            this.hideTimer = setTimeout(function () {
              thisElement.tooltip.hide();
            }, 200);
          }
        } else if (this.$ref && this.$ref.hasClass('rt-target')) {
          this.$ref.removeClass('rt-target');
          if (activatedByClick) {
            $body.off('click.rt touchstart.rt', this.onBodyClick);
          }
        }
      };

      this.showRef = function ($element, ePageX, ePageY) {
        // Popups gadget
        if (window.pg) {
          disableRt();
          return;
        }

        if (this.tooltip && !this.tooltip.$content.length) {
          return;
        }

        var tooltipInitiallyPresent = this.tooltip && this.tooltip.isPresent;

        function reallyShow(html) {
          var viewportTop, refOffsetTop, teHref;

          if (!thisElement.tooltip) {
            thisElement.tooltip = new Tooltip(thisElement, html);
            if (!thisElement.tooltip.$content.length) {
              return;
            }
          }

          // If this tooltip is called from inside another tooltip. We can't define it
          // in the constructor since a ref can be cloned but have the same Tooltip object;
          // so, Tooltip.parent is a floating value.
          thisElement.tooltip.parent = thisElement.$element.closest('.rt-tooltip').data('tooltip');
          if (thisElement.tooltip.parent && thisElement.tooltip.parent.disappearing) {
            return;
          }

          thisElement.tooltip.show();

          if (tooltipInitiallyPresent) {
            if (thisElement.tooltip.$element.hasClass('rt-tooltip-above')) {
              thisElement.tooltip.$element.addClass(CLASSES.FADE_IN_DOWN);
            } else {
              thisElement.tooltip.$element.addClass(CLASSES.FADE_IN_UP);
            }
            return;
          }

          thisElement.tooltip.calculatePosition(ePageX, ePageY);

          $window.on('resize.rt', thisElement.onWindowResize);
        }

        // We redefine this.$element here because e.target can be a reference link inside
        // a reference tooltip, not a link that was initially assigned to this.$element
        this.$element = $element;

        if (this.type === 'commentedText') {
          this.$element.attr('title', '');
        }

        if (activatedByClick) {
          if (tooltipInitiallyPresent ||
            (this.$ref && this.$ref.hasClass('rt-target'))
          ) {
            return;
          } else {
            setTimeout(function () {
              $body.on('click.rt touchstart.rt', thisElement.onBodyClick);
            }, 0);
          }
        }

				GetIPATable($element, function(html) {
	        if (activatedByClick || tooltipInitiallyPresent) {
	          reallyShow(html);
	        } else {
	          this.showTimer = setTimeout(function(){reallyShow(html)}, delay);
	        }
				})
      };

      this.onBodyClick = function (e) {
        if (!thisElement.tooltip && !thisElement.$ref.hasClass('rt-target')) {
          return;
        }

        var $current = $(e.target);

        function contextMatchesParameter(parameter) {
          return this === parameter;
        }

        // The last condition is used to determine cases when a clicked tooltip is the current
        // element's tooltip or one of its descendants
        while ($current.length &&
          (!$current.hasClass('rt-tooltip') ||
            !$current.data('tooltip') ||
            !$current.data('tooltip').upToTopParent(
              contextMatchesParameter, [thisElement.tooltip],
              true
            )
          )
        ) {
          $current = $current.parent();
        }
        if (!$current.length) {
          thisElement.hideRef();
        }
      };

      this.onWindowResize = function () {
        thisElement.tooltip.calculatePosition();
      };
    }

    function Tooltip(thisElement, html) {
      var tooltip = this;

      // This variable can change: one tooltip can be called from a harvard-style reference link
      // that is put into different tooltips
      this.thisElement = thisElement;

      // this.id = 'rt-' + this.thisElement.$originalElement.attr('id');
      this.$content =  $($.parseHTML(html))

      if (!this.$content.length) {
        return;
      }

      this.insideWindow = Boolean(this.thisElement.$element.closest('.oo-ui-window').length);

      this.$element = $('<div>')
        .addClass('rt-tooltip')
        .attr('id', this.id)
        .attr('role', 'tooltip')
        .data('tooltip', this);
      if (this.insideWindow) {
        this.$element.addClass('rt-tooltip-insideWindow');
      }

      // We need the $content interlayer here in order for the settings icon to have correct
      // margins
      this.$content = this.$content
        .wrapAll('<div>')
        .parent()
        .addClass('rt-tooltipContent')
        .addClass('mw-parser-output')
        .appendTo(this.$element);

      if (!activatedByClick) {
        this.$element
          .mouseenter(function () {
            if (!tooltip.disappearing) {
              tooltip.upToTopParent(function () {
                this.show();
              });
            }
          })
          .mouseleave(function (e) {
            // https://stackoverflow.com/q/47649442 workaround. Relying on relatedTarget
            // alone has pitfalls: when alt-tabbing, relatedTarget is empty too
            if (CLIENT_NAME !== 'chrome' ||
              (!e.originalEvent ||
                e.originalEvent.relatedTarget !== null ||
                !tooltip.clickedTime ||
                $.now() - tooltip.clickedTime > 50
              )
            ) {
              tooltip.upToTopParent(function () {
                this.thisElement.hideRef();
              });
            }
          })
          .click(function () {
            tooltip.clickedTime = $.now();
          });
      }

      // Tooltip tail element is inside tooltip content element in order for the tooltip
      // not to disappear when the mouse is above the tail
      this.$tail = $('<div>')
        .addClass('rt-tooltipTail')
        .prependTo(this.$element);

      this.disappearing = false;

      this.show = function () {
				console.log('show')
        this.disappearing = false;
        clearTimeout(this.thisElement.hideTimer);
        clearTimeout(this.thisElement.removeTimer);

        this.$element
          .removeClass(CLASSES.FADE_OUT_DOWN)
          .removeClass(CLASSES.FADE_OUT_UP);

        if (!this.isPresent) {
          $body.append(this.$element);
        }

        this.isPresent = true;
      };

      this.hide = function () {
        var tooltip = this;

        tooltip.disappearing = true;

        if (tooltip.$element.hasClass('rt-tooltip-above')) {
          tooltip.$element
            .removeClass(CLASSES.FADE_IN_DOWN)
            .addClass(CLASSES.FADE_OUT_UP);
        } else {
          tooltip.$element
            .removeClass(CLASSES.FADE_IN_UP)
            .addClass(CLASSES.FADE_OUT_DOWN);
        }

        tooltip.thisElement.removeTimer = setTimeout(function () {
          if (tooltip.isPresent) {
            tooltip.$element.detach();

            tooltip.$tail.css('left', '');

            if (activatedByClick) {
              $body.off('click.rt touchstart.rt', tooltip.thisElement.onBodyClick);
            }
            $window.off('resize.rt', tooltip.thisElement.onWindowResize);

            tooltip.isPresent = false;
          }
        }, 200);
      };

      this.calculatePosition = function (ePageX, ePageY) {
        var teElement, teOffsets, teOffset, tooltipTailOffsetX, tooltipTailLeft,
          offsetYCorrection = 0;

        this.$tail.css('left', '');

        teElement = this.thisElement.$element.get(0);
        if (ePageX !== undefined) {
          tooltipTailOffsetX = ePageX;
          teOffsets = teElement.getClientRects &&
            teElement.getClientRects() ||
            teElement.getBoundingClientRect();
          if (teOffsets.length > 1) {
            for (var i = teOffsets.length - 1; i >= 0; i--) {
              if (ePageY >= Math.round($window.scrollTop() + teOffsets[i].top) &&
                ePageY <= Math.round(
                  $window.scrollTop() + teOffsets[i].top + teOffsets[i].height
                )
              ) {
                teOffset = teOffsets[i];
              }
            }
          }
        }

        if (!teOffset) {
          teOffset = teElement.getClientRects &&
            teElement.getClientRects()[0] ||
            teElement.getBoundingClientRect();
        }
        teOffset = {
          top: $window.scrollTop() + teOffset.top,
          left: $window.scrollLeft() + teOffset.left,
          width: teOffset.width,
          height: teOffset.height
        };
        if (!tooltipTailOffsetX) {
          tooltipTailOffsetX = (teOffset.left * 2 + teOffset.width) / 2;
        }
        if (CLIENT_NAME === 'msie' && this.thisElement.type === 'supRef') {
          offsetYCorrection = -Number(
            this.thisElement.$element.parent().css('font-size').replace('px', '')
          ) / 2;
        }
        this.$element.css({
          top: teOffset.top - this.$element.outerHeight() - 7 + offsetYCorrection,
          left: tooltipTailOffsetX - 20,
          right: ''
        });

        // Is it squished against the right side of the page?
        if (this.$element.offset().left + this.$element.outerWidth() > $window.width() - 1) {
          this.$element.css({
            left: '',
            right: 0
          });
          tooltipTailLeft = tooltipTailOffsetX - this.$element.offset().left - 5;
        }

        // Is a part of it above the top of the screen?
        if (teOffset.top < this.$element.outerHeight() + $window.scrollTop() + 6) {
          this.$element
            .removeClass('rt-tooltip-above')
            .addClass('rt-tooltip-below')
            .addClass(CLASSES.FADE_IN_UP)
            .css({
              top: teOffset.top + teOffset.height + 9 + offsetYCorrection
            });
          if (tooltipTailLeft) {
            this.$tail.css('left', (tooltipTailLeft + 12) + 'px');
          }
        } else {
          this.$element
            .removeClass('rt-tooltip-below')
            .addClass('rt-tooltip-above')
            .addClass(CLASSES.FADE_IN_DOWN)
            // A fix for cases when a tooltip shown once is then wrongly positioned when it
            // is shown again after a window resize. We just repeat what is above.
            .css({
              top: teOffset.top - this.$element.outerHeight() - 7 + offsetYCorrection
            });
          if (tooltipTailLeft) {
            // 12 is the tail element width/height
            this.$tail.css('left', tooltipTailLeft + 'px');
          }
        }
      };

      // Run some function for all the tooltips up to the top one in a tree. Its context will be
      // the tooltip, while its parameters may be passed to Tooltip.upToTopParent as an array
      // in the second parameter. If the third parameter passed to ToolTip.upToTopParent is true,
      // the execution stops when the function in question returns true for the first time,
      // and ToolTip.upToTopParent returns true as well.
      this.upToTopParent = function (func, parameters, stopAtTrue) {
        var returnValue,
          currentTooltip = this;

        do {
          returnValue = func.apply(currentTooltip, parameters);
          if (stopAtTrue && returnValue) {
            break;
          }
        } while (currentTooltip = currentTooltip.parent);

        if (stopAtTrue) {
          return returnValue;
        }
      };
    }

    if (!enabled) {
      addEnableLink();
      return;
    }

    teSelector = REF_LINK_SELECTOR;
    $content.find(teSelector).each(function () {
      new TooltippedElement($(this));
    });
  }

  settingsString = mw.cookie.get('RTsettings', '');
  if (settingsString) {
    settings = settingsString.split('|');
    enabled = Boolean(Number(settings[0]));
    delay = Number(settings[1]);
    activatedByClick = Boolean(Number(settings[2]));
    // The forth value was added later, so we provide for a default value. See comments below
    // for why we use "IS_TOUCHSCREEN && IS_MOBILE".
    tooltipsForComments = settings[3] === undefined ?
      IS_TOUCHSCREEN && IS_MOBILE :
      Boolean(Number(settings[3]));
  } else {
    enabled = true;
    delay = 200;
    // Since the mobile browser check is error-prone, adding IS_MOBILE condition here would probably
    // leave cases where a user interacting with the browser using touches doesn't know how to call
    // a tooltip in order to switch to activation by click. Some touch-supporting laptop users
    // interacting by touch (though probably not the most popular use case) would not be happy too.
    activatedByClick = IS_TOUCHSCREEN;
    // Arguably we shouldn't convert native tooltips into gadget tooltips for devices that have
    // mouse support, even if they have touchscreens (there are laptops with touchscreens).
    // IS_TOUCHSCREEN check here is for reliability, since the mobile check is prone to false
    // positives.
    tooltipsForComments = IS_TOUCHSCREEN && IS_MOBILE;
  }

  rt($('.mw-body'))

}())