User:GhostInTheMachine/CommentsInLocalTime.js

/**
 * CommentsInLocalTime -- cloned from [[User:Gary/comments_in_local_time.js]]
 * 
 * See -- [[User:GhostInTheMachine/CommentsInLocalTime]]
 */

console.log('CommentsInLocalTime loaded');

$(function() {
  console.log('CommentsInLocalTime starting');

  // Add a leading zero if necessary to form a 2 digit string

  function addLeadingZero(i) {
  	return i > 9 ? ''+i : '0'+i;
  }

  function convertMonthToNumber(month) {
    return new Date(`${month} 1, 2001`).getMonth();
  }

  function getDates(time) {
    const [, oldHour, oldMinute, oldDay, oldMonth, oldYear] = time;

    // Today
    const today = new Date();

    // Yesterday
    const yesterday = new Date();

    yesterday.setDate(yesterday.getDate() - 1);

    // Tomorrow
    const tomorrow = new Date();

    tomorrow.setDate(tomorrow.getDate() + 1);

    // Set the date entered.
    const newTime = new Date();

    newTime.setUTCFullYear(oldYear, convertMonthToNumber(oldMonth), oldDay);
    newTime.setUTCHours(oldHour);
    newTime.setUTCMinutes(oldMinute);

    return { time: newTime, today, tomorrow, yesterday };
  }

  /**
   * Determine whether to use the singular or plural word, and use that.
   *
   * @param {string} term Original term
   * @param {number} count Count of items
   * @param {string} plural Pluralized term
   * @returns {string} The word to use
   */
  function pluralize(term, count, plural = null) {
  	return count < 2 ? term : plural ? plural : term+'s';
  }

  class CommentsInLocalTime {
    constructor() {
      this.language = '';
      this.LocalComments = {};

      /**
       * Settings
       */
      this.settings();

      this.language = this.setDefaultSetting(
        'language',
        this.LocalComments.language
      );

      // These values are also reflected in the documentation:
      // https://en.wikipedia.org/wiki/Wikipedia:Comments_in_Local_Time#Default_settings
      this.setDefaultSetting({
        dateDifference: true,
        dateFormat: 'dmy',
        dayOfWeek: true,
        dropDays: 0,
        dropMonths: 0,
        timeFirst: true,
        twentyFourHours: false,
      });
    }

    adjustTime(originalTimestamp, search) {
      const { time, today, tomorrow, yesterday } = getDates(
        originalTimestamp.match(search)
      );

      // A string matching the date pattern was found, but it cannot be
      // converted to a Date object. Return it with no changes made.
      if (Number.isNaN(time)) {
        return [originalTimestamp, ''];
      }

      const date = this.determineDateText({
        time,
        today,
        tomorrow,
        yesterday,
      });

      const { ampm, hour } = this.getHour(time);
      const minute = addLeadingZero(time.getMinutes());
      const finalTime = `${hour}:${minute}${ampm}`;
      let returnDate;

      // Determine the time offset.
      const utcValue = (-1 * time.getTimezoneOffset()) / 60;
      const utcOffset =
        utcValue >= 0 ? `+${utcValue}` : `−${Math.abs(utcValue.toFixed(1))}`;

      if (this.LocalComments.timeFirst) {
        returnDate = `${finalTime}, ${date} (UTC${utcOffset})`;
      } else {
        returnDate = `${date}, ${finalTime} (UTC${utcOffset})`;
      }

      return { returnDate, time };
    }

    convertNumberToMonth(number) {
      return [
        this.language.January,
        this.language.February,
        this.language.March,
        this.language.April,
        this.language.May,
        this.language.June,
        this.language.July,
        this.language.August,
        this.language.September,
        this.language.October,
        this.language.November,
        this.language.December,
      ][number];
    }

    createDateText({ day, month, time, today, year }) {
      // Calculate day of week
      const dayNames = [
        this.language.Sunday,
        this.language.Monday,
        this.language.Tuesday,
        this.language.Wednesday,
        this.language.Thursday,
        this.language.Friday,
        this.language.Saturday,
      ];
      const dayOfTheWeek = dayNames[time.getDay()];
      let descriptiveDifference = '';
      let last = '';

      // Create a relative descriptive difference
      if (this.LocalComments.dateDifference) {
        ({ descriptiveDifference, last } = this.createRelativeDate(
          today,
          time
        ));
      }

      const monthName = this.convertNumberToMonth(time.getMonth());

      // Format the date according to user preferences
      let formattedDate = '';

      switch (this.LocalComments.dateFormat.toLowerCase()) {
        case 'dmy':
          formattedDate = `${day} ${monthName} ${year}`;

          break;
        case 'mdy':
          formattedDate = `${monthName} ${day}, ${year}`;

          break;
        default:
          formattedDate = `${year}-${month}-${addLeadingZero(day)}`;
      }

      let formattedDayOfTheWeek = '';

      if (this.LocalComments.dayOfWeek) {
        formattedDayOfTheWeek = `, ${last}${dayOfTheWeek}`;
      }

      return formattedDate + formattedDayOfTheWeek + descriptiveDifference;
    }

    /**
     * Create relative date data.
     *
     * @param {Date} today Today
     * @param {Date} time The timestamp from a comment
     * @returns {Object.<string, *>} Relative date data
     */
    createRelativeDate(today, time) {
      /**
       * The time difference from today, in milliseconds.
       *
       * @type {number}
       */
      const millisecondsAgo = today.getTime() - time.getTime();

      /**
       * The number of days ago, that we will display. It's not necessarily the
       * total days ago.
       *
       * @type {number}
       */
      let daysAgo = Math.abs(Math.round(millisecondsAgo / 1000 / 60 / 60 / 24));
      const { differenceWord, last } = this.relativeText({
        daysAgo,
        millisecondsAgo,
      });

      // This method of computing the years and months is not exact. However,
      // it's better than the previous method that used 1 January + delta days.
      // That was usually quite off because it mapped the second delta month to
      // February, which has only 28 days. This method is usually not more than
      // one day off, except perhaps over very distant dates.

      /**
       * The number of months ago, that we will display. It's not necessarily
       * the total months ago.
       *
       * @type {number}
       */
      let monthsAgo = Math.floor((daysAgo / 365) * 12);

      /**
       * The total amount of time ago, in months.
       *
       * @type {number}
       */
      const totalMonthsAgo = monthsAgo;

      /**
       * The number of years ago that we will display. It's not necessarily the
       * total years ago.
       *
       * @type {number}
       */
      let yearsAgo = Math.floor(totalMonthsAgo / 12);

      if (totalMonthsAgo < this.LocalComments.dropMonths) {
        yearsAgo = 0;
      } else if (this.LocalComments.dropMonths > 0) {
        monthsAgo = 0;
      } else {
        monthsAgo -= yearsAgo * 12;
      }

      if (daysAgo < this.LocalComments.dropDays) {
        monthsAgo = 0;
        yearsAgo = 0;
      } else if (this.LocalComments.dropDays > 0 && totalMonthsAgo >= 1) {
        daysAgo = 0;
      } else {
        daysAgo -= Math.floor((totalMonthsAgo * 365) / 12);
      }

      const descriptiveParts = [];

      // There is years text to add.
      if (yearsAgo > 0) {
        descriptiveParts.push(
          `${yearsAgo} ${pluralize(
            this.language.year,
            yearsAgo,
            this.language.years
          )}`
        );
      }

      // There is months text to add.
      if (monthsAgo > 0) {
        descriptiveParts.push(
          `${monthsAgo} ${pluralize(
            this.language.month,
            monthsAgo,
            this.language.months
          )}`
        );
      }

      // There is days text to add.
      if (daysAgo > 0) {
        descriptiveParts.push(
          `${daysAgo} ${pluralize(
            this.language.day,
            daysAgo,
            this.language.days
          )}`
        );
      }

      return {
        descriptiveDifference: ` (${descriptiveParts.join(
          ', '
        )} ${differenceWord})`,
        last,
      };
    }

    determineDateText({ time, today, tomorrow, yesterday }) {
      // Set the date bits to output.
      const year = time.getFullYear();
      const month = addLeadingZero(time.getMonth() + 1);
      const day = time.getDate();

      // Return 'today' or 'yesterday' if that is the case
      if (
        year === today.getFullYear() &&
        month === addLeadingZero(today.getMonth() + 1) &&
        day === today.getDate()
      ) {
        return this.language.Today;
      }

      if (
        year === yesterday.getFullYear() &&
        month === addLeadingZero(yesterday.getMonth() + 1) &&
        day === yesterday.getDate()
      ) {
        return this.language.Yesterday;
      }

      if (
        year === tomorrow.getFullYear() &&
        month === addLeadingZero(tomorrow.getMonth() + 1) &&
        day === tomorrow.getDate()
      ) {
        return this.language.Tomorrow;
      }

      return this.createDateText({ day, month, time, today, year });
    }

    getHour(time) {
      let ampm;
      let hour = parseInt(time.getHours(), 10);

      if (this.LocalComments.twentyFourHours) {
        ampm = '';
        hour = addLeadingZero(hour);
      } else {
        // Output am or pm depending on the date.
        ampm = hour <= 11 ? ' am' : ' pm';

        if (hour > 12) {
          hour -= 12;
        } else if (hour === 0) {
          hour = 12;
        }
      }

      return { ampm, hour };
    }

    relativeText({ daysAgo, millisecondsAgo }) {
      let differenceWord = '';
      let last = '';

      // The date is in the past.
      if (millisecondsAgo >= 0) {
        differenceWord = this.language.ago;

        if (daysAgo <= 7) {
          last = `${this.language.last} `;
        }

        // The date is in the future.
      } else {
        differenceWord = this.language['from now'];

        if (daysAgo <= 7) {
          last = `${this.language.this} `;
        }
      }

      return { differenceWord, last };
    }

    replaceText(node, search) {
      if (!node) {
        return;
      }

      // Check if this is a text node.
      if (node.nodeType === 3) {
        let parent = node.parentNode;

        const parentNodeName = parent.nodeName;

        if (['CODE', 'PRE'].includes(parentNodeName)) {
          return;
        }

        const value = node.nodeValue;
        const matches = value.match(search);

        // Stick with manipulating the DOM directly rather than using jQuery.
        // I've got more than a 100% speed improvement afterward.
        if (matches) {
          // Only act on the first timestamp we found in this node. This is
          // really a temporary fix for the situation in which there are two or
          // more timestamps in the same node.
          const [match] = matches;
          const position = value.search(search);
          const stringLength = match.toString().length;
          const beforeMatch = value.substring(0, position);
          const afterMatch = value.substring(position + stringLength);
          const { returnDate, time } = this.adjustTime(
            match.toString(),
            search
          );
          const timestamp = time ? time.getTime() : '';

          // Is the "timestamp" attribute used for microformats?
          const span = document.createElement('span');

          span.className = 'localcomments';
          span.style.fontSize = '95%';
          span.style.whiteSpace = 'nowrap';
          span.setAttribute('timestamp', timestamp);
          span.title = match;
          span.append(document.createTextNode(returnDate));

          parent = node.parentNode;
          parent.replaceChild(span, node);

          const before = document.createElement('span');

          before.className = 'before-localcomments';
          before.append(document.createTextNode(beforeMatch));

          const after = document.createElement('span');

          after.className = 'after-localcomments';
          after.append(document.createTextNode(afterMatch));

          parent.insertBefore(before, span);
          parent.insertBefore(after, span.nextSibling);
        }
      } else {
        const children = [];
        let child;

        [child] = node.childNodes;

        while (child) {
          children.push(child);
          child = child.nextSibling;
        }

        // Loop through children and run this func on it again, recursively.
        children.forEach((child2) => {
          this.replaceText(child2, search);
        });
      }
    }

    run() {
      if (
        ['', 'MediaWiki', 'Special'].includes(
          mw.config.get('wgCanonicalNamespace')
        )
      ) {
        console.log('CommentsInLocalTime wrong namespace');
        return;
      }

      console.log('CommentsInLocalTime right namespace');

      // Check for disabled URLs.
      const isDisabledUrl = ['action=history'].some((disabledUrl) =>
        document.location.href.includes(disabledUrl)
      );

      if (isDisabledUrl) {
        console.log('CommentsInLocalTime disabled URL');
        return;
      }

      this.replaceText(
      	document.querySelector('#mw-content-text .mw-parser-output'),
        /(\d{1,2}):(\d{2}), (\d{1,2}) ([A-Z][a-z]+) (\d{4}) \(UTC\)/
      );
    }

    setDefaultSetting(...args) {
      // There are no arguments.
      if (args.length === 0) {
        return false;
      }

      // The first arg is an object, so just set that data directly onto the
      // settings object. like {setting 1: true, setting 2: false}
      if (typeof args[0] === 'object') {
        const [settings] = args;

        // Loop through each setting.
        Object.keys(settings).forEach((name) => {
          const value = settings[name];

          if (typeof this.LocalComments[name] === 'undefined') {
            this.LocalComments[name] = value;
          }
        });

        return settings;
      }

      // The first arg is a string, so use the first arg as the settings key,
      // and the second arg as the value to set it to.
      const [name, setting] = args;

      if (typeof this.LocalComments[name] === 'undefined') {
        this.LocalComments[name] = setting;
      }

      return this.LocalComments[name];
    }

    /**
     * Set the script's settings.
     *
     * @returns {undefined}
     */
    settings() {
      // The user has set custom settings, so use those.
      if (window.LocalComments) {
        this.LocalComments = window.LocalComments;
      }

      /**
       * Language
       *
       * LOCALIZING THIS SCRIPT
       * To localize this script, change the terms below,
       * to the RIGHT of the colons, to the correct term used in that language.
       *
       * For example, in the French language,
       *
       * 'Today' : 'Today',
       *
       * would be
       *
       * 'Today' : "Aujourd'hui",
       */
      this.LocalComments.language = {
        // Relative terms
        Today: 'Today',
        Yesterday: 'Yesterday',
        Tomorrow: 'Tomorrow',
        last: 'last',
        this: 'this',

        // Days of the week
        Sunday: 'Sunday',
        Monday: 'Monday',
        Tuesday: 'Tuesday',
        Wednesday: 'Wednesday',
        Thursday: 'Thursday',
        Friday: 'Friday',
        Saturday: 'Saturday',

        // Months of the year
        January: 'January',
        February: 'February',
        March: 'March',
        April: 'April',
        May: 'May',
        June: 'June',
        July: 'July',
        August: 'August',
        September: 'September',
        October: 'October',
        November: 'November',
        December: 'December',

        // Difference words
        ago: 'ago',
        'from now': 'from now',

        // Date phrases
        year: 'year',
        years: 'years',
        month: 'month',
        months: 'months',
        day: 'day',
        days: 'days',
      };
    }
  }

  // Check if we've already ran this script.
  if (window.commentsInLocalTimeWasRun) {
    console.log('CommentsInLocalTime already run');
    return;
  }

  window.commentsInLocalTimeWasRun = true;

  const commentsInLocalTime = new CommentsInLocalTime();

  commentsInLocalTime.run();
});

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.