משתמש:Bharel/סקריפטים/64-new.js

מתוך ויקיפדיה, האנציקלופדיה החופשית

הערה: לאחר הפרסום, ייתכן שיהיה צורך לנקות את זיכרון המטמון (cache) של הדפדפן כדי להבחין בשינויים.

  • פיירפוקס / ספארי: להחזיק את המקש Shift בעת לחיצה על טעינה מחדש (Reload) או ללחוץ על צירוף המקשים Ctrl-F5 או Ctrl-R (במחשב מק: ⌘-R).
  • גוגל כרום: ללחוץ על צירוף המקשים Ctrl-Shift-R (במחשב מק: ⌘-Shift-R).
  • אינטרנט אקספלורר / אדג': להחזיק את המקש Ctrl בעת לחיצה על רענן (Refresh) או ללחוץ על צירוף המקשים Ctrl-F5.
  • אופרה: ללחוץ על Ctrl-F5.
/* Rewritten and refactored by Bar Harel (User:Bharel) based on old 64.js */
/* globals importStylesheet, mw, $, confirm, prompt, console, alert */
importStylesheet('משתמש:Bharel/סקריפטים/64-new.css');

const script64Manager = function(){
  "use strict";
  const Manager = {};
  
  const SettingTypes = Manager.SettingTypes = {
    NORMAL: "NORMAL",
    ANONYMOUS: "ANONYMOUS",
    GENERAL: "GENERAL"
  };

  const SettingPage = {
    [SettingTypes.NORMAL]:
        ('משתמש:' + mw.config.get('wgUserName') +
         '/סקריפט 64/UserSettings.js'),
    [SettingTypes.ANONYMOUS]:
        ('משתמש:' + mw.config.get('wgUserName') +
         '/סקריפט 64/AnonSettings.js'),
    [SettingTypes.GENERAL]:
        ('משתמש:' + mw.config.get('wgUserName') +
         '/סקריפט 64/GeneralSettings.js'),
  };

  const DefaultSettingPage = {
    [SettingTypes.NORMAL]: 'משתמש:Bharel/סקריפט 64/DefaultUserSettings.js',
    [SettingTypes.ANONYMOUS]: 'משתמש:Bharel/סקריפט 64/DefaultAnonSettings.js',
    [SettingTypes.GENERAL]:
        'משתמש:Bharel/סקריפט 64/DefaultGeneralSettings.js',
  };
  
  const ALL_TEMPLATES_PAGE = 'משתמש:Bharel/סקריפט 64/AllTemplates.js';

  const LOCAL_STORAGE_PREFIX = mw.config.get("wgUserName") + "Script64";

  const settingsCache = {};

  // The manager will call these handlers with itself as first argument
  // until one of the handlers returns a true value.
  Manager.handlers = [];

  function purgeCache_(settingType){
    window.localStorage.removeItem(LOCAL_STORAGE_PREFIX+settingType);
    delete settingsCache[settingType];
  }

  /* Gets the settings for the current user

  Args:
    settingType: Which setting should we get out of SettingTypes.

  Returns:
    A jQuery ajax promise, which, if successful,
    calls .done with settings as a JSON.
  */
  function getSettings_(settingType) {
    // Fetch from site
    return $.ajax({
      url: mw.util.wikiScript(),
      data: {
        action: 'raw',
        title: SettingPage[settingType]
      },
      dataType: 'json'
    });
  }

  /* Sets the settings for the current user

  Also, if successful, resets the settings cache.

  Args:
    anonymous: Should we set the anonymous settings or not
    settings: JSON with the settings

  Returns:
    A jQuery ajax promise.
  */
  Manager.setSettings = function(settingType, settings) {
    let api = new mw.Api({ajax: {dataType: 'json'}});
    return api
        .postWithToken('csrf', {
          action: 'edit',
          title: SettingPage[settingType],
          summary: 'Script 64 - Set settings',
          text: JSON.stringify(settings),
        })
        .done(() =>{
          // Remove from storage and cache
          purgeCache_(settingType);
        });
  };

  /* Gets the settings for the current user.

  Unlike getSettings_, this sets and returns the default settings if current
  user's settings do not exist.

  Args:
    anonymous: Should we get the anonymous settings or not.

  Returns:
    A Promise object that upon .then, calls with the settings.
  */
  Manager.getSettings = function(settingType) {
    return new Promise((resolve, reject) => {
      // Check if already exists in cache
      if (settingsCache[settingType])
        return resolve(settingsCache[settingType]);

      // Check if already exists in storage
      const localItem =
          window.localStorage.getItem(LOCAL_STORAGE_PREFIX+settingType);
      if (localItem){
        // Set the settings from storage into the cache and resolve
        return resolve(settingsCache[settingType] = JSON.parse(localItem));
      }
      
      // Attempt to first get the user settings
      getSettings_(settingType)
          .done(settings => {
            // Cache in the local storage and resolve
            window.localStorage.setItem(
                LOCAL_STORAGE_PREFIX+settingType,
                JSON.stringify(settings));
            settingsCache[settingType] = settings;
            resolve(settings);
          })
          .fail(error => {
            // Assuming failure is because they don't exist
            // Different failures should throw an error.
            if (error.status != 404) throw error;

            // Attempt to fetch default settings
            $.ajax({
              url: mw.util.wikiScript(),
              data: {
                action: 'raw',
                title:
                    DefaultSettingPage[settingType]
              },
              dataType: 'json'
            })
            .done(settings => {
              // Success fetching default settings.
              // Set the user's settings.
              Manager.setSettings(settingType, settings);
              resolve(settings);
            })
            .fail(error => reject(error));
          });
    });
  };

  /* Helper function to get settings according to the given name */
  Manager.withSettingsByName = function(userName, callback) {
    return Manager.withSettings(
        mw.util.isIPAddress(userName) ?
            SettingTypes.ANONYMOUS :
            SettingTypes.NORMAL,
        callback);
  };

  /* Helper function to pass a callback to be called with the settings */
  Manager.withSettings = function(settingType, callback) {
    return Manager.getSettings(settingType).then(callback);
  };

  /* Calls the callback with all available templates.

  Returns:
    jQuery Promise for fetching available templates.
  */
  Manager.withAvailableTemplates = function(callback) {
    return $
        .ajax({
          url: mw.util.wikiScript(),
          data: {action: 'raw', title: ALL_TEMPLATES_PAGE},
          dataType: 'json',
          cache: true
        })
        .done(callback)
        .fail(error => { throw error; });
  };

  /* Calls the callback with all available templates separated by user type

  This is the struct passed to the callback:
  {
    "anonymous": anonymousTemplates,
    "normal": normalTemplates
  }

  Returns:
    jQuery Promise for fetching available templates.
  */
  Manager.withSeparateAvailableTemplates = function(callback) {
    return Manager.withAvailableTemplates(availableTemplates => {
      const anonymousTemplates = {};
      const normalTemplates = {};

      // For every type of template
      for (const [templateType, templateList] of
           Object.entries(availableTemplates)) {

        // Create a new list for that specific template type
        const anonymousList = anonymousTemplates[templateType] = [];
        const normalList = normalTemplates[templateType] = [];

        // Sort the templates to the correct lists
        for (const template of templateList) {
          switch (template.anon) {
            case true:
              anonymousList.push(template);
              break;
            case false:
              normalList.push(template);
              break;
            default:
              anonymousList.push(template);
              normalList.push(template);
              break;
          }
        }
      }

      callback({
        [SettingTypes.ANONYMOUS]: anonymousTemplates,
        [SettingTypes.NORMAL]: normalTemplates
      });
    });
  };

  /* Creates a span with warning links according to the given templates

  Upon clicking the links, they will warn the given user using the clicked
  template.

  Args:
    userName: User to warn upon clicking the link.
    templates: An array of templates.

  Each template is an object with the following attributes:
    name: template name to show as the link text
    template: template's page name (to be included in user talk)
    description: hover description
    The following attributes are optional:
      watch: To watch or unwatch user talk page.
      noPage: Don't include the page as first argument in the template.
      title: By default, the new section's title is the page name.
      Override the default title naming and put this one instead.
      noTitle: Don't create a new section when adding the template.
      Does not work with title.
      noLinkTitle: Section title should not be a link to the page.
      Does not work with title.
      askExtra:
        Object with one of the following:
          confirm: confirm dialog.
            Accepting will add param to template
          prompt: prompt dialog.
            Submitting will adds param together with prompt value

  Returns:
    The created span element.
  */
  Manager.createLinksElement = function(userName, templates) {
    // Create the container span
    const containerElement = document.createElement('span');

    const fillContainer = () => {
      const fillFragment = document.createDocumentFragment();
      const seperatorNode = document.createTextNode(' | ');

      // Add an event listener that handles inner <a> tags
      containerElement.addEventListener('click', event => {
        // Travels up the DOM tree
        for (let target = event.target; target !== event.currentTarget;
             target = target.parentNode) {
          
          // As soon as it finds an object with a warning template,
          // activates the warning.
          if (target.warningTemplate) {
            event.stopPropagation();
            event.preventDefault();
            return this.activateWarning(userName, target.warningTemplate);
          }
        }
      });

      // Starts the container
      fillFragment.appendChild(document.createTextNode('('));

      /* Appends a template link together with it's event handler */
      function appendTemplateElement(template) {
        let element = document.createElement('a');
        element.href = '#';
        element.insertAdjacentText('beforeend', template.name);
        element.warningTemplate = template;
        fillFragment.appendChild(element);
      }

      // Add elements to containerElement.
      // Does not add the seperator before first element
      appendTemplateElement(templates[0]);
      for (let i = 1; i < templates.length; i++) {
        fillFragment.appendChild(seperatorNode.cloneNode());
        appendTemplateElement(templates[i]);
      }

      // Ends the container
      fillFragment.appendChild(document.createTextNode(')'));
      containerElement.appendChild(fillFragment);
    };
    Manager.withSettings(SettingTypes.GENERAL, generalSettings => {
      // According to the orientation, we can tell
      // between desktop and mobile
      
      // Shouldn't hide
      if (!generalSettings.hideOptions[
          window.orientation === undefined ? 'desktop' : 'mobile']){
        return fillContainer();
      }
      // Should hide
      const showLinksButton = document.createElement('button');
      showLinksButton.insertAdjacentText('beforeend', 'הצג תבניות');

      // Upon click, wait 400ms (to avoid misclick) and then show links.
      showLinksButton.addEventListener(
        'click',
        () => window.setTimeout(
          () => {
            containerElement.removeChild(showLinksButton);
            fillContainer();
          },
          400)
      );

      containerElement.appendChild(showLinksButton);
    });
    return containerElement;
  };

  /* Activates the given template on the given user

  See createLinksElement for template object attributes
  */
  Manager.activateWarning = function(userName, template) {
    let templateString = template.template; // Full template string
    const pageName = mw.config.get('wgPageName').replace(/_/g, ' ');

    // Make sure to only link and not include categories & files.
    const linkPageName =
        ([6, 14].includes(
            mw.config.get('wgNamespaceNumber')) ? ':' : '') + pageName;

    // If we should include a link to the page as first template argument.
    if (!template.noPage) templateString += '|' + linkPageName;

    // Needs extra information?
    if (template.askExtra) {
      // Need a prompt
      if (template.askExtra.prompt) {
        const userInput = prompt(template.askExtra.prompt);
        // Add parameter if prompt was accepted.
        if (userInput !== null) {
          templateString += (template.askExtra.param + userInput.trim());
        }

      // Need a confirmation
      } else if (
          template.askExtra.confirm && confirm(template.askExtra.confirm)) {
        // Add parameter if confirmed.
        templateString += template.askExtra.param;
      }
    }

    let fullText = '{{' + templateString + '}} ~~' + '~~\n';

    // If template has a specific title
    if (template.title) fullText = '\n==' + template.title + '==\n' + fullText;

    // If template has a default title
    else if (!template.noTitle) {
      fullText =
          '\n==' +
          (template.noLinkTitle ? pageName : '[[' + linkPageName + ']]') +
          '==\n' + fullText;
    }

    // Add the resulting string to the user talk page.
    const page = new this.pageHandler('שיחת משתמש:' + userName);

    if (template.prepend)
      page.prepend(fullText, template.template);
    else
      page.append(fullText, template.template);

    if (template.watch !== undefined) page.watch = template.watch;

    page.submit()
        .then(() => mw.notify(
            ' תבנית "' + template.template +
            '" נרשמה בשיחת משתמש:' + userName))
        .catch(error => alert('Error saving: ' + String(error)));

  };

  /* Class to simplify editing pages */
  Manager.pageHandler = class pageHandler{
    constructor(pageTitle) {
      this.action = 'edit';
      this.title = pageTitle;
      this.summary = [];
      this.format = 'json';
      this.token = mw.user.tokens.get('csrfToken');
    }

    /* Append content to page */
    append(content, summary) {
      this.appendtext = (this.appendtext ? this.appendtext : '') + content;
      if (summary) this.summary.push(summary);
      return this;
    }

    /* Prepend content to page */
    prepend(content, summary) {
      this.prependtext = (this.prependtext ? this.prependtext : '') + content;
      if (summary) this.summary.push(summary);
      return this;
    }

    /* Get the watch flag for the page */
    get watch() {
      if (this.watchlist === undefined) return undefined;
      return this.watchlist === 'watch';
    }

    /* Set the watch flag for the page */
    set watch(value) { this.watchlist = value ? 'watch' : 'unwatch'; }

    /* Submit the edits

    Note: Object is unusable afterwards.

    Returns:
      Promise object with the data or error received.
    */
    submit() {
      this.summary = this.summary.join(', ');
      return new Promise((resolve, reject) => {
        $.post(mw.util.wikiScript('api'), this, data => {
           if (data.error) return reject(data.error);
           if (data.edit && data.edit.result == 'Success') return resolve(data);
           throw data;
         }).fail(error => { throw error; });
      });
    }

  };

  

  Manager.SettingsDialog = {
    DIALOG_BOX_TEMPLATE: [
      '<div id="setting64" title="הגדרות סקריפט 64">',
        '<div id="setting64Tabs">',
          '<ul>',
            '<li><a href="#setting64AnonTab">אנונימים</a></li>',
            '<li><a href="#setting64NormalTab">משתמשים</a></li>',
          '</ul>',
          '<div id="setting64AnonTab"></div>',
          '<div id="setting64NormalTab"></div>',
        '</div>',
        '<div id="setting64General">',
          'הסתרת תבניות:',
          '<ul id="setting64HideOptions">',
            '<li>',
              '<input type=checkbox id="setting64HideDesktop" />',
              '<label for="setting64HideDesktop">במחשב נייח</label>',
            '</li>',
            '<li>',
              '<input type=checkbox id="setting64HideMobile" />',
              '<label for="setting64HideMobile">בנייד</label>',
            '</li>',
          '</ul>',
          '<div id="setting64AdvancedSettings" style="display: none">',
            '<button id="setting64AddManual">הוספת תבנית בצורה ידנית',
            '</button></br>',
            '<button id="setting64PurgeCache">נקה מטמון</button></br>',
          '</div>',
          '<input type=checkbox id="setting64ShowAdvanced" />',
          '<label for="setting64ShowAdvanced">הצג הגדרות מתקדמות</label>',
        '</div>',
      '</div>'].join(''),

    /* Open the dialog

    Args:
      manager: script64Manager (Needed for helper functions)
    */
    openDialog: function() {
      mw.loader.using(['jquery.ui'])
          .done(() => {
            if (this.$dialogBox) {
              this.$dialogBox.dialog('open');
              return;
            }

            this.createDialog().dialog('open');
          });
    },

    /* Pops open a new template creation dialog */
    newTemplateDialog: function() {
      const $dialog = $([
        '<div id="setting64NewTemplate">',
          '<form>',
            '<span>כל השדות חובה.</span></br>',
            '<label>שם: <input type="text" id="setting64NameField">',
            '</label><br>',
            '<label>תבנית: <input type"text" id="setting64TemplateField">',
            '</label><br>',
            'הוסף לתבניות <select id="setting64LocationField"></select>.<br>',
            'הוסף עבור:</br>',
            '<input type="radio" name="setting64CategoryRadio" ',
            'value="anon">אנונימים</br>',
            '<input type="radio" name="setting64CategoryRadio" ',
            'value="normal">משתמשים</br>',
            '<input type="radio" name="setting64CategoryRadio" ',
            'value="both" checked="checked">שניהם<br>',
            '<input type="submit" value="הוספה" tabindex="-1" ',
            'style="position:absolute; top:-1000px">',
          '</form>',
        '</div>'
      ].join(''));
      
      const selectMenu = $dialog[0].querySelector('#setting64LocationField');
      
      // Add all options for the select menu.
      [ // [title, warningType]
        ['אזהרה', 'rollbackWarnings'],
        ['מחיקה', 'deletionWarnings'],
        ['חסימה', 'blockWarnings']
      ].forEach(([title, warningType]) => {
        const optionElement = document.createElement('option');
        optionElement.warningType = warningType;
        optionElement.text = title;
        selectMenu.appendChild(optionElement);
      });

      const form = $dialog[0].firstChild;

      // Save the template
      const saveTemplate = () => {
        const template = {};

        // Extract name and template, check they are not empty.
        template.name = document.getElementById('setting64NameField').value;
        template.template =
            document.getElementById('setting64TemplateField').value;

        if (template.name.length === 0 || template.template.length === 0){
          form.firstChild.classList.add('ui-state-error');
          return;
        }

        template.description = 'הוספת תבנית ' + template.name;

        // Add to anon normal or both
        const selectedRadio =
            form.querySelector('input[name="setting64CategoryRadio"]:checked')
            .value;

        const warningType =
            selectMenu.options[selectMenu.selectedIndex].warningType;

        // Append the templates to the appropriate sortables
        switch (selectedRadio){
          case 'anon':
            this.sortables[SettingTypes.ANONYMOUS][warningType]
            .appendTemplate(template);
            break;
          case 'normal':
            this.sortables[SettingTypes.NORMAL][warningType]
            .appendTemplate(template);
            break;
          case 'both':
            this.sortables[SettingTypes.ANONYMOUS][warningType]
            .appendTemplate(template);
            this.sortables[SettingTypes.NORMAL][warningType]
            .appendTemplate(template);
            break;
          default:
            throw 'Error: Unknown radio selected';
        }
        $dialog.dialog('close');
        $dialog.empty();
      };

      // Support the submit button (enter key)
      form.addEventListener('submit', event => {
        event.preventDefault();
        saveTemplate();
      });

      // Create the dialog and show it.
      $dialog.dialog({
        autoOpen: true,
        height: 300,
        width: 250,
        modal: true,
        buttons: {
          'הוסף': saveTemplate,
          'סגור': () => {
            $dialog.dialog('close');
            $dialog.empty();
          }
        }
      });
    },

    /* Represents a single warning sortable

    The warning sortable is a jquery ui sortable

    Args:
      templates: list of templates for the sortable
      available: list of available template options

    Attributes:
      mainNode: Main node (in this case a document fragment) that holds
      the sortable. Append this to wherever you want the sortable to exist.
    */
    WarningSortable: class WarningSortable{
      constructor(templates, available){
        // Initialize the sortable
        const mainNode = this.mainNode = document.createDocumentFragment();
        const sortable = this.sortable_ = document.createElement('ul');
        const selectElement =
            this.selectElement_ = document.createElement('select');
        const templateNames = this.templateNames_ = new Set();
        const appendButton = document.createElement('span');

        sortable.className = 'script-64-sortable';
        appendButton.className = 'ui-icon ui-icon-plus';

        // Binds close button to "li" removal from DOM.
        sortable.addEventListener('click', event => {
          // Travels up the DOM tree
          for (let target = event.target; target !== event.currentTarget;
               target = target.parentNode) {
            // As soon as we find the close button
            // remove it's parent from the sortable
            if (target.className === 'ui-icon ui-icon-close') {
              const targetParent = target.parentNode;

              event.stopPropagation();
              event.preventDefault();

              sortable.removeChild(targetParent);

              // Remove it from currently used names
              // and append it to select menu.
              templateNames.delete(targetParent.warningTemplate.name);
              this.appendAvailableTemplate(targetParent.warningTemplate);
              return;
            }
          }
        });

        // Append all templates to the sortable.
        templates.forEach(this.appendTemplate, this);

        // Append all available warnings as options in
        // the select element.
        available.forEach(this.appendAvailableTemplate, this);

        const $sortable = $(sortable);

        // Bind the append button to append the selected item.
        appendButton.addEventListener('click', () => {
          const optionElement =
              selectElement.options[selectElement.selectedIndex];
          
          templateNames.delete(optionElement.warningTemplate.name);
          this.appendTemplate(optionElement.warningTemplate);
          
          // Remove the option from the select menu.
          selectElement.removeChild(optionElement);
          $sortable.sortable();
        });

        // Create the sortable
        $sortable.sortable();
        $sortable.disableSelection();

        // Create the main fragment.
        mainNode.appendChild(sortable);
        mainNode.appendChild(selectElement);
        mainNode.appendChild(appendButton);
      }

      // Appends an available template to the select menu.
      appendAvailableTemplate(template){
          // Avoid duplicate templates.
          if (this.templateNames_.has(template.name)) return;

          const optionElement = document.createElement('option');

          optionElement.text = template.name;
          optionElement.warningTemplate = template;

          this.selectElement_.appendChild(optionElement);
          this.templateNames_.add(template.name);
      }

      // Appends a single template as a list item.
      appendTemplate(template){
          // Avoid duplicate templates.
          if (this.templateNames_.has(template.name)) return;
          const listItem = document.createElement('li');
          const closeButton = document.createElement('span');
          const arrowIcon = document.createElement('span');
          
          closeButton.className = 'ui-icon ui-icon-close';
          arrowIcon.className = 'ui-icon ui-icon-arrowthick-2-n-s';

          // Build the list item
          listItem.className = 'ui-state-default';
          listItem.warningTemplate = template;
          listItem.appendChild(closeButton);
          listItem.appendChild(arrowIcon);
          listItem.insertAdjacentText('beforeend', template.name);
          
          // Append it.
          this.sortable_.appendChild(listItem);
          this.templateNames_.add(template.name);
      }

      get templates() {
        return [...this.sortable_.children].map(li => li.warningTemplate);
      }
    },

    /* Creates the dialog box

    Returns:
      The dialog box as a jQuery element
    */
    createDialog: function() {
      const $dialogBox = this.$dialogBox = $(this.DIALOG_BOX_TEMPLATE);
      this.sortables = {};

      /* Creates the sortables

      Args:
          currentSettings: a settings JSON
          availableWarnings: available warnings JSON

      Returns:
          [DocumentFragment containing warning sortables,
          Object mapping warningType: sortableInstance]
      */
      const createWarningSortables = (currentSettings, availableWarnings) => {
        const fragment = document.createDocumentFragment();
        const warningTemplate = document.createElement('div');
        const sortables = {};

        warningTemplate.className = 'script-64-warnings';
        warningTemplate.appendChild(document.createElement('h2'));

        // Create all three sortables.
        [ // [title, warningType]
          ['תבניות לאזהרה', 'rollbackWarnings'],
          ['תבניות למחיקת דפים', 'deletionWarnings'],
          ['תבניות לחסימה', 'blockWarnings']
        ].forEach(([title, warningType]) => {
          const sortable = new this.WarningSortable(
            currentSettings[warningType], availableWarnings[warningType]);
          sortables[warningType] = sortable;

          const warningElement = warningTemplate.cloneNode(true);
          warningElement.firstChild.textContent = title;
          warningElement.dataset.settingName = warningType;

          warningElement.appendChild(sortable.mainNode);
          fragment.appendChild(warningElement);
        });

        return [fragment, sortables];
      };

      const anonymousTab = $dialogBox.find('#setting64AnonTab');
      const normalTab = $dialogBox.find('#setting64NormalTab');
      
      /* Build the anonymous and user tabs

      Args:
        advanced: Show advanced settings
      */
      const showTabs = (advanced=false) => {
        /* Get a combined version of the available templates
  
        Sets all templates to be available in all cases.

        Args:
          availableTemplates: the availableTemplates JSON

        Returns:
          an availableTemplates JSON with all templates available in
          all cases.
        */
        function combineTemplates(availableTemplates){

          const allTemplates = [].concat(...Object.values(availableTemplates));
          const combinedTemplates = {};

          // Clone and set all templates.
          for (const key of Object.keys(availableTemplates)){
            combinedTemplates[key] = [...allTemplates];
          }

          return combinedTemplates;
        }

        Manager.withSeparateAvailableTemplates(separateTemplates => {
          Manager.withSettings(SettingTypes.ANONYMOUS, settings => {
            let fragment, anonSortables;
            if (advanced){
              // Create the advanced warning sortable
              [fragment, anonSortables] =
                  createWarningSortables(
                      settings,
                      combineTemplates(
                          separateTemplates[SettingTypes.ANONYMOUS]));
                  

            } else {
              // Create the normal warning sortable
              [fragment, anonSortables] =
                  createWarningSortables(
                      settings,
                      separateTemplates[SettingTypes.ANONYMOUS]);
            }
            anonymousTab.append(fragment);
            this.sortables[SettingTypes.ANONYMOUS] = anonSortables;
          });

          Manager.withSettings(SettingTypes.NORMAL, settings => {
            let fragment, normalSortables;
            if (advanced){
              // Create the advanced warning sortable
              [fragment, normalSortables] =
                  createWarningSortables(
                      settings,
                      combineTemplates(
                          separateTemplates[SettingTypes.NORMAL]));
                  

            } else {
              // Create the normal warning sortable
              [fragment, normalSortables] =
                  createWarningSortables(
                      settings,
                      separateTemplates[SettingTypes.NORMAL]);
            }
            normalTab.append(fragment);
            this.sortables[SettingTypes.NORMAL] = normalSortables;
          });
        });
      };

      showTabs();

      // If advanced checkbox is checked, reload with advanced settings.
      $dialogBox.find('#setting64ShowAdvanced').change(event => {
        const advnaced = event.currentTarget.checked;
        anonymousTab.empty();
        normalTab.empty();
        showTabs(advnaced);
        
        if (advnaced)
            $dialogBox.find('#setting64AdvancedSettings')
            .css("display", "block");
        else
            $dialogBox.find('#setting64AdvancedSettings')
            .css("display", "none");
      });

      // General settings
      Manager.withSettings(SettingTypes.GENERAL, generalSettings => {
        $dialogBox.find('#setting64HideDesktop')
            .prop('checked', generalSettings.hideOptions.desktop);
        $dialogBox.find('#setting64HideMobile')
            .prop('checked', generalSettings.hideOptions.mobile);
      });

      $dialogBox.find("#setting64AddManual")
      .click(this.newTemplateDialog.bind(this));

      $dialogBox.find("#setting64PurgeCache").click(() => {
        Object.values(SettingTypes).forEach(purgeCache_, this.Manager);
        console.log("Purged cache.");
      });

      $dialogBox.dialog({
        autoOpen: false,
        height: 400,
        width: 350,
        modal: true,
        buttons: {
          'שמירה': () => {
            // Save and notify in case of success
            this.saveBox()
                .done(() => mw.notify('הגדרות סקריפט 64 נשמרו בהצלחה.'));
            $dialogBox.dialog('close');
          },
          'סגירה': () => $dialogBox.dialog('close')
        }
      });

      // Activate the tabs
      $dialogBox.find('#setting64Tabs').tabs();
      return $dialogBox;
    },

    /* Save the data in the dialog box.

    Returns:
        A jQuery promise. .done() will be called when settings were saved
        successfully.
    */
    saveBox: function() {
      function extractSettings(sortables) {
        const settings = {};
        for (const [warningType, sortable] of Object.entries(sortables)){
          settings[warningType] = sortable.templates;
        }
        return settings;
      }

      const generalSettings = {
        hideOptions: {
          desktop: document.getElementById('setting64HideDesktop').checked,
          mobile: document.getElementById('setting64HideMobile').checked,
        }
      };

      return $.when(
          // Set anonymous settings
          Manager.setSettings(
              SettingTypes.ANONYMOUS,
              extractSettings(this.sortables[SettingTypes.ANONYMOUS])),

          // Set... Well... Guess.
          Manager.setSettings(
              SettingTypes.NORMAL,
              extractSettings(this.sortables[SettingTypes.NORMAL])),

          Manager.setSettings(SettingTypes.GENERAL, generalSettings)
          );
    },
  };

  /* Start point for the script. */
  Manager.loadManager = function() {
    this.mainElement = document.getElementById('mw-content-text');

    mw.util.addPortletLink('p-tb', '#', 'הגדרות סקריפט 64').onclick =
        this.SettingsDialog.openDialog.bind(this.SettingsDialog);

    // Execute handlers until one of them returns a true value
    for (const handler of this.handlers) {
      if (handler(this)) break;
    }
  };


  return Manager;
}();

// Check if we blocked a user
script64Manager.handlers.push(function(manager) {
  'use strict';
  if (mw.config.get('wgCanonicalSpecialPageName') !== 'Block') return;

  /* Args: Element to insert, Node to insert the element after */
  function insertSibling(insert, node) {
    return node.parentNode.insertBefore(insert, node.nextSibling);
  }

  // The name of the link is the userName.
  const userlinkNode = manager.mainElement.getElementsByTagName('a')[0];
  const userName = userlinkNode.firstChild.nodeValue;
  const spaceNode = document.createTextNode(' ');

  // Add a spacer
  insertSibling(spaceNode, userlinkNode.parentNode);

  // Create the main warning element and insert it.
  manager.withSettingsByName(userName, settings => {
    insertSibling(
        manager.createLinksElement(userName, settings.blockWarnings),
        spaceNode);
  });

  return true;
});


// Check if it's a diff
script64Manager.handlers.push(function(manager) {
  'use strict';
  // Diffs have this element
  const diffElement = document.getElementById('mw-diff-ntitle2');

  if (diffElement === null) return;

  // The bi-directional element holds a text node with the userName
  const userName =
      diffElement.getElementsByTagName('bdi')[0].firstChild.nodeValue;

  // Create a new paragraph to hold all links.
  const paragraph = document.createElement('p');

  // Create the main warning element, insert into the paragraph, and append it.
  manager.withSettingsByName(userName, settings => {
    paragraph.appendChild(
        manager.createLinksElement(userName, settings.rollbackWarnings));
    diffElement.appendChild(paragraph);
  });

  return true;
});

// Check for a rollback
script64Manager.handlers.push(function(manager) {
  'use strict';
  if (mw.config.get('wgAction') !== 'rollback') return;

  // userName is the text of the first bi-directional element.
  const userElement = manager.mainElement.getElementsByTagName('bdi')[0];

  if (!userElement) return;  // Rollback failed

  const userToolLinksNode = userElement.parentNode.nextElementSibling;
  const userName = userElement.firstChild.nodeValue;

  // Assuming we're in the right place
  console.assert(
      userToolLinksNode.className == 'mw-usertoollinks',
      userToolLinksNode.className);

  /* Args: Element to insert, Node to insert the element after */
  function insertSibling(insert, node) {
    return node.parentNode.insertBefore(insert, node.nextSibling);
  }

  // Create some warning indicators
  const fragment = document.createDocumentFragment();

  fragment.appendChild(document.createTextNode(' ('));

  // Full status span (Block and warn)
  const statusSpan = document.createElement('span');
  statusSpan.id = 'script-64-status-span';

  const warnedSpan = document.createElement('span');
  warnedSpan.textContent = 'הוזהר';
  statusSpan.appendChild(warnedSpan);

  statusSpan.insertAdjacentText('beforeend', ' ');

  const blockSpan = document.createElement('span');
  blockSpan.textContent = 'חסום';
  statusSpan.appendChild(blockSpan);

  fragment.appendChild(statusSpan);
  fragment.appendChild(document.createTextNode(') '));

  // Append the links element to the fragment and insert it.
  manager.withSettingsByName(userName, settings => {
    fragment.appendChild(
        manager.createLinksElement(userName, settings.rollbackWarnings));
    insertSibling(fragment, userToolLinksNode);
  });

  const api = new mw.Api();

  // If a block was found, turn on the indicators.
  function indicateBlocks(data){
    if (data && data.query && data.query.blocks && data.query.blocks &&
        data.query.blocks.length) {
      // Light the indicators! We have a freak on the loose!
      blockSpan.className = 'script-64-activated-indicator';
      blockSpan.title =
          'נחסם על ידי ' + decodeURIComponent(data.query.blocks[0].by);
    }
  }

  // Get block status and light indicators accordingly
  if (mw.util.isIPAddress(userName)) {
    api.get({list: 'blocks', bkip: userName}).done(indicateBlocks);
  } else {
    api.get({list: 'blocks', bkusers: userName}).done(indicateBlocks);
  }

  // Modify warning indicator by checking the user's talk pages'
  // recent revision summaries for warning templates.
  api.get({
    action: 'query',
    titles: 'שיחת משתמש:' + userName,
    prop: 'revisions',
    indexpageids: true,
    rvprop: ['user', 'timestamp', 'comment'],
    rvlimit: 5,
    format: 'json',
  })
  .done(data => {
    const pageid = data.query.pageids[0];
    if (pageid === '-1') return;  // User talk page not found

    manager.withAvailableTemplates(templates => {
      const revisionList = data.query.pages[pageid].revisions;
      const timeThreshold = mw.now() - 30 * 24 * 60 * 60 * 1000;  // 30 days

      // Concat all available templates into one big list
      const templatesList = [].concat(...Object.values(templates));

      // Extract the template's name from all templates and turn into a set.
      const templateNames =
          new Set(templatesList.map(template => template.template));

      // Iterate over all revisions, matching revision summary
      // with a template name. If a match is found, and the revision isn't
      // older than the set threshold, light the indicators.
      for (const revision of revisionList) {
        const revisionDate = new Date(revision.timestamp);

        // Revision passed the time threshold
        // Since they are sorted by date, all later revisions also
        // passed the threshold. Stop the iteration.
        if (revisionDate < timeThreshold) return;

        // Match summary with template name.
        if (!templateNames.has(revision.comment)) continue;

        // Found a match. Light the indicators! Ahhhh!!! Panic.
        warnedSpan.title =
            `הוזהר על ידי ${revision.user} ` +
            `בתאריך ${revisionDate.toLocaleString()}.`;

        warnedSpan.className = 'script-64-activated-indicator';
        return;
          }
        });
      });
  return true;
});

// Check for page deletion
script64Manager.handlers.push(function(manager) {
  'use strict';
  // If it's not a deletion
  if (mw.config.get('wgAction') !== 'delete' ||

      // Or user is not a sysop
      !mw.config.get('wgUserGroups').includes('sysop') ||

      // Or deletion was not successful (entered confirmation page)
      document.getElementById('deleteconfirm'))
    return;

  // Query the API for last 20 deleted revisions
  $.getJSON(
      mw.util.wikiScript('api'),
      {
        action: 'query',
        list: 'deletedrevs',
        drlimit: 20,
        titles: mw.config.get('wgPageName'),
        drprop: 'user',
        format: 'json'
      },
      function(data) {
        const users = new Set();
        if (data && data.query && data.query.deletedrevs)
          // Add all users who made a change in the last 20 revisions.
          data.query.deletedrevs.forEach(
              (deletedrev) => deletedrev.revisions.forEach(
                  (revision) => users.add(revision.user)));
        else
          throw 'No deleted revisions..?';

        // Create a container for all users who modified the page.
        const container = document.createElement('p');
        container.insertAdjacentText(
            'beforeend',
            `את הדף ${mw.config.get('wgPageName')} ערכו העורכים הבאים:`);

        /* Creates basic links for a user.

        Link to the User page, User Talk, Contributions and block.

        Returns:
            a DocumentFragment.
        */
        function createUserLinks(userName) {
          function createLink(url, text) {
            const element = document.createElement('a');

            element.setAttribute('href', url);
            element.textContent = text;

            return element;
          }
          const fragment = document.createDocumentFragment();

          fragment.appendChild(
              createLink(mw.util.getUrl('User:' + userName), userName));

          fragment.appendChild(document.createTextNode(' ('));
          fragment.appendChild(
              createLink(mw.util.getUrl('User Talk:' + userName), 'שיחה'));
          fragment.appendChild(document.createTextNode(' | '));
          fragment.appendChild(createLink(
              mw.util.getUrl('Special:Contributions/' + userName), 'תרומות'));
          fragment.appendChild(document.createTextNode(' | '));
          fragment.appendChild(
              createLink(mw.util.getUrl('Special:Block/' + userName), 'חסימה'));
          fragment.appendChild(document.createTextNode(' ) '));

          return fragment;
        }

        // For each user, adds his links and warnings.
        users.forEach(
            // No need to worry, withSettingsByName is cached.
            user => manager.withSettingsByName(user, settings => {
              const userContainer = document.createElement('p');

              userContainer.appendChild(createUserLinks(user));
              userContainer.appendChild(
                  manager.createLinksElement(
                      user, settings.deletionWarnings));

              container.appendChild(userContainer);
            }));

        // Append the entire container.
        manager.mainElement.appendChild(container);
      });
  return true;
});


/* Function to load all necessary polyfills.

Returns:
  a combined jQuery Promise for when all polyfills finish loading.
*/
script64Manager.loadPolyfills = function(){
  'use strict';
  /* jshint unused:false */
  
  // Loads a single script with caching turned on.
  function loadScript(url){
    return $.ajax({
      url: url,
      dataType: 'script',
      cache: true,
    });
  }

  /***** Starting Polyfills *****/
  /* jshint ignore:start */

  const reduce =
    Function.bind.call(Function.call, Array.prototype.reduce);
  const isEnumerable =
    Function.bind.call(Function.call, Object.prototype.propertyIsEnumerable);
  const concat =
    Function.bind.call(Function.call, Array.prototype.concat);
  const keys = Reflect.ownKeys;

  /** Object.values polyfill **/
  if (!Object.values) {
    Object.values = function values(O) {
      return reduce(
        keys(O),
        (v, k) => concat(
            v, typeof k === 'string' && isEnumerable(O, k) ? [O[k]] : []),
        []);
    };
  }

  /** Object.entries polyfill **/
  if (!Object.entries) {
    Object.entries = function entries(O) {
      return reduce(
        keys(O),
        (e, k) => concat(
            e,
            typeof k === 'string' &&
            isEnumerable(O, k) ? [[k, O[k]]] : []),
        []);
    };
  }

  /** Array.includes polyfill **/
  if (!Array.prototype.includes) {
    Object.defineProperty(Array.prototype, 'includes', {
      value: function(searchElement, fromIndex) {
        if (this == null) {
          throw new TypeError('"this" is null or not defined');
        }
        var o = Object(this);
        var len = o.length >>> 0;
        if (len === 0) {
          return false;
        }
        var n = fromIndex | 0;
        var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
        while (k < len) {
          if (o[k] === searchElement) {
            return true;
          }
          k++;
        }
        return false;
      }
    });
  }

  /* jshint ignore:end */
  /***** End of polyfills *****/
  return $.when();
};

/*** Load script ***/
script64Manager.loadPolyfills().done(() => {
  'use strict';
  // Wait for page to load.
  if (!['loaded', 'complete','interactive'].includes(document.readyState))
      document.addEventListener(
          'DOMContentLoaded',
          script64Manager.loadManager.bind(script64Manager));
  // Activate if page was already loaded.
  else script64Manager.loadManager();
});