ശ്രദ്ധിക്കുക: സേവ് ചെയ്തശേഷം മാറ്റങ്ങൾ കാണാനായി താങ്കൾക്ക് ബ്രൗസറിന്റെ കാഷെ ഒഴിവാക്കേണ്ടി വന്നേക്കാം.

  • ഫയർഫോക്സ് / സഫാരി: Reload ബട്ടൺ അമർത്തുമ്പോൾ Shift കീ അമർത്തി പിടിക്കുകയോ, Ctrl-F5 അല്ലെങ്കിൽ Ctrl-R (മാക്കിന്റോഷിൽ ⌘-R ) എന്ന് ഒരുമിച്ച് അമർത്തുകയോ ചെയ്യുക
  • ഗൂഗിൾ ക്രോം: Ctrl-Shift-R (മാക്കിന്റോഷിൽ ⌘-Shift-R ) അമർത്തുക
  • ഇന്റർനെറ്റ് എക്സ്പ്ലോറർ: Refresh ബട്ടൺ അമർത്തുമ്പോൾ Ctrl കീ അമർത്തിപിടിക്കുക. അല്ലെങ്കിൽ Ctrl-F5 അമർത്തുക
  • ഓപ്പറ: Menu → Settings എടുക്കുക (മാക്കിൽ Opera → Preferences) എന്നിട്ട് Privacy & security → Clear browsing data → Cached images and files ചെയ്യുക.
/**
 * WikiEditMobile — Mobile visual section editor for Wikipedia
 *
 * A MediaWiki user script providing a touch-friendly WYSIWYG editor for
 * article sections on mobile.
 * built for the Minerva skin with Parsoid HTML round-trip conversion.
 *
 * Edit flow per section:
 *   mw.Api (wikitext) → Parsoid (HTML) → contentEditable editor
 *   → on save: Parsoid (wikitext) → mw.Api (edit)
 *
 * Installation — add ONE of these to your user JS page:
 *
 *   [[User:Ranjithsiji/minerva.js]]  ← mobile only (recommended)
 *   [[User:Ranjithsiji/common.js]]   ← all skins
 *
 *   importScript('User:YOU/WikiEditMobile.js');
 *
 * Requirements: logged-in Wikipedia account, Parsoid REST API available
 * (true for all Wikimedia wikis).
 */

/* global mw */
(function () {
  'use strict';

  // ── Guards ──────────────────────────────────────────────────────────────────
  if (mw.config.get('wgAction') !== 'view')   return;
  if (!mw.config.get('wgIsArticle'))          return;
  if (mw.config.get('wgNamespaceNumber') !== 0) return; // main namespace only
  if (!mw.config.get('wgUserId'))             return; // must be logged in

  // ── Config ───────────────────────────────────────────────────────────────────
  var PAGE   = mw.config.get('wgPageName');          // e.g. "Albert_Einstein"
  var USER   = mw.config.get('wgUserName');
  var ORIGIN = location.origin;                       // e.g. "https://en.wikipedia.org"
  var REST   = ORIGIN + '/api/rest_v1/transform';

  // ── Toolbar button definitions (nicEdit-inspired) ───────────────────────────
  // Each entry is either a separator { sep:true } or a button descriptor.
  // special:true buttons open a dialog pane (like nicEditorAdvancedButton)
  // instead of calling execCommand directly.
  // Unicode labels — all glyphs are standard Unicode code points, no image
  // files needed (contrast with nicEdit which uses a sprite GIF).
  //
  //  𝐁  U+1D401  Mathematical Bold Capital B
  //  𝐼  U+1D408  Mathematical Italic Capital I
  //  H¹ / H² / H³  plain letter + superscript digit (U+00B9, U+00B2, U+00B3)
  //  ① U+2460  Circled Digit One   (ordered list)
  //  ⁍  U+204D  Black Right Pointing Bullet  (unordered list)
  //  🖼  U+1F5BC Frame With Picture           (insert image)
  //  ⊞  U+229E  Squared Plus / 4-cell grid   (insert table)
  //  🔗  U+1F517 Link Symbol                  (insert link)
  var BUTTONS = [
    { cmd: 'bold',                label: '𝐁',  title: 'Bold (Ctrl+B)' },
    { cmd: 'italic',              label: '𝐼',  title: 'Italic (Ctrl+I)' },
    { sep: true },
    { cmd: 'formatBlock', val: 'H1', label: 'H¹', title: 'Heading 1' },
    { cmd: 'formatBlock', val: 'H2', label: 'H²', title: 'Heading 2' },
    { cmd: 'formatBlock', val: 'H3', label: 'H³', title: 'Heading 3' },
    { sep: true },
    { cmd: 'insertOrderedList',   label: '①',  title: 'Numbered list' },
    { cmd: 'insertUnorderedList', label: '⁍',  title: 'Bullet list' },
    { sep: true },
    { cmd: 'insertMedia', label: '🖼', title: 'Insert image', special: true },
    { cmd: 'insertTable', label: '⊞',  title: 'Insert table', special: true },
    { cmd: 'createLink',  label: '🔗', title: 'Insert link',  special: true },
  ];

  // ── Keyboard shortcuts (nicEdit also maps Ctrl+key to commands) ─────────────
  var KEY_MAP = { b: 'bold', i: 'italic', z: 'undo', y: 'redo' };

  // ── Embedded CSS ─────────────────────────────────────────────────────────────
  var CSS = [
    /* Floating action button */
    '#wem-fab{',
      'position:fixed;bottom:72px;right:16px;z-index:9990;',
      'width:52px;height:52px;border-radius:50%;',
      'background:#3366cc;color:#fff;font-size:22px;line-height:1;',
      'border:none;box-shadow:0 3px 10px rgba(0,0,0,.35);',
      'cursor:pointer;touch-action:manipulation;',
      'display:flex;align-items:center;justify-content:center;',
      'transition:transform .12s;',
    '}',
    '#wem-fab:active{transform:scale(.9)}',

    /* Full-screen overlay */
    '#wem-overlay{',
      'position:fixed;inset:0;z-index:9995;',
      'display:flex;flex-direction:column;',
      'background:#fff;font-family:sans-serif;',
      'overscroll-behavior:contain;',
    '}',
    '#wem-overlay[hidden]{display:none}',

    /* Header bar */
    '#wem-hdr{',
      'background:#3366cc;color:#fff;',
      'padding:10px 12px;display:flex;align-items:center;gap:10px;',
      'min-height:52px;flex-shrink:0;',
    '}',
    '#wem-back{',
      'background:none;border:none;color:#fff;font-size:24px;',
      'cursor:pointer;padding:4px 8px;line-height:1;',
      'touch-action:manipulation;flex-shrink:0;',
    '}',
    '#wem-hdr-title{',
      'flex:1;font-size:17px;font-weight:600;',
      'white-space:nowrap;overflow:hidden;text-overflow:ellipsis;',
    '}',
    '#wem-save{',
      'background:#27ae60;border:none;color:#fff;',
      'padding:7px 14px;border-radius:4px;font-size:14px;font-weight:600;',
      'cursor:pointer;touch-action:manipulation;flex-shrink:0;',
      'transition:opacity .15s;',
    '}',
    '#wem-save:disabled{opacity:.45;cursor:default}',

    /* Section list */
    '#wem-sections{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch}',
    '.wem-sec{',
      'display:flex;align-items:center;',
      'padding:15px 16px;border-bottom:1px solid #eaecf0;',
      'cursor:pointer;-webkit-tap-highlight-color:transparent;',
    '}',
    '.wem-sec:active{background:#f0f4ff}',
    '.wem-sec-lv{color:#72777d;font-size:12px;min-width:30px;font-family:monospace}',
    '.wem-sec-nm{flex:1;font-size:16px}',
    '.wem-sec-arr{color:#a2a9b1;font-size:20px;padding-left:8px}',

    /* Editor pane */
    '#wem-editor{flex:1;display:flex;flex-direction:column;overflow:hidden;min-height:0}',

    /* Toolbar — sits at the very bottom of the overlay, just above the
       soft keyboard.  Horizontally scrollable so all 9 buttons fit on
       narrow screens without wrapping.  border-top separates from content. */
    '#wem-toolbar{',
      'display:flex;align-items:center;flex-wrap:nowrap;',
      'overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;',
      'background:#f8f9fa;',
      'border-top:2px solid #c8ccd1;',
      /* Push content above the iPhone home-indicator notch */
      'padding:6px 6px calc(6px + env(safe-area-inset-bottom, 0px)) 6px;',
      'gap:4px;flex-shrink:0;',
    '}',
    '#wem-toolbar[hidden]{display:none}',
    '#wem-toolbar::-webkit-scrollbar{display:none}',

    /* Buttons — 44 px tall (Apple HIG minimum touch target) for bottom-of-
       screen thumb access.  Icon font-size bumped so Unicode glyphs are
       clearly readable at arm's length. */
    '.wem-btn{',
      'flex-shrink:0;min-width:44px;height:44px;padding:0 8px;',
      'background:#fff;border:1px solid #c8ccd1;border-radius:6px;',
      'font-size:16px;cursor:pointer;touch-action:manipulation;',
      'display:flex;align-items:center;justify-content:center;',
      'transition:background .1s;white-space:nowrap;',
    '}',
    '.wem-btn:active,.wem-btn.wem-active{background:#d0d7f0;border-color:#3366cc}',
    '.wem-sep{width:1px;height:32px;background:#c8ccd1;flex-shrink:0;margin:0 2px}',

    /* The contentEditable editing surface */
    '#wem-editable-wrap{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:12px;min-height:0}',
    '#wem-editable{',
      'min-height:200px;outline:none;word-wrap:break-word;',
      'line-height:1.6;font-size:15px;',
    '}',
    '#wem-editable h2{font-size:1.3em;border-bottom:1px solid #eaecf0;padding-bottom:4px;margin:12px 0 6px}',
    '#wem-editable h3{font-size:1.15em;margin:10px 0 4px}',
    '#wem-editable a{color:#3366cc}',
    '#wem-editable ul,#wem-editable ol{padding-left:1.6em;margin:4px 0}',
    '#wem-editable p{margin:4px 0}',
    '#wem-editable table{border-collapse:collapse;font-size:13px}',
    '#wem-editable td,#wem-editable th{border:1px solid #a2a9b1;padding:4px 6px}',

    /* Status bar */
    '#wem-status{',
      'padding:5px 12px;font-size:12px;color:#54595d;',
      'background:#f8f9fa;border-top:1px solid #eaecf0;',
      'min-height:26px;flex-shrink:0;line-height:16px;',
    '}',
    '#wem-status.wem-err{color:#d33}',
    '#wem-status.wem-ok{color:#27ae60}',

    /* ── Dialog (like nicEditorPane, but full-width bottom sheet on mobile) ── */
    /* Backdrop dims the editor while the dialog is open */
    '#wem-dialog-backdrop{',
      'position:absolute;inset:0;z-index:100;',
      'background:rgba(0,0,0,.45);',
      'display:flex;align-items:flex-end;',    /* dialog slides up from bottom */
    '}',
    '#wem-dialog-backdrop[hidden]{display:none}',
    '#wem-dialog{',
      'width:100%;background:#fff;',
      'border-radius:16px 16px 0 0;',
      'padding:20px 16px 32px;',
      'box-sizing:border-box;',
      'box-shadow:0 -4px 24px rgba(0,0,0,.18);',
    '}',
    '#wem-dialog-title{',
      'font-size:17px;font-weight:700;margin:0 0 16px;color:#202122;',
    '}',
    '.wem-field{margin-bottom:14px}',
    '.wem-field label{',
      'display:block;font-size:13px;font-weight:600;',
      'color:#54595d;margin-bottom:4px;',
    '}',
    '.wem-field input,.wem-field select{',
      'width:100%;box-sizing:border-box;',
      'padding:10px 12px;font-size:15px;',
      'border:1px solid #c8ccd1;border-radius:6px;',
      'background:#fff;appearance:none;-webkit-appearance:none;',
    '}',
    '.wem-field input:focus,.wem-field select:focus{',
      'outline:none;border-color:#3366cc;',
      'box-shadow:0 0 0 2px rgba(51,102,204,.25);',
    '}',
    '.wem-field-row{display:flex;gap:10px}',
    '.wem-field-row .wem-field{flex:1;margin-bottom:0}',
    '.wem-field-row .wem-field.narrow{flex:0 0 80px}',
    '#wem-dialog-footer{',
      'display:flex;gap:10px;margin-top:20px;',
    '}',
    '#wem-dialog-footer button{',
      'flex:1;padding:12px;border-radius:8px;',
      'font-size:15px;font-weight:600;cursor:pointer;border:none;',
      'touch-action:manipulation;',
    '}',
    '#wem-dlg-cancel{background:#f8f9fa;color:#202122;border:1px solid #c8ccd1!important}',
    '#wem-dlg-insert{background:#3366cc;color:#fff}',
    '#wem-dlg-insert:disabled{opacity:.5}',
  ].join('');

  // ──────────────────────────────────────────────────────────────────────────────
  // API layer
  // ──────────────────────────────────────────────────────────────────────────────

  var mwApi = new mw.Api();

  /**
   * Returns the list of sections for the current page.
   * @returns {Promise<Array>} Array of section objects with {index, line, toclevel}
   */
  function getSections() {
    return mwApi.get({
      action: 'parse',
      page: PAGE,
      prop: 'sections',
      format: 'json',
      formatversion: 2,
    }).then(function (d) { return d.parse.sections; });
  }

  /**
   * Fetches the raw wikitext for a single section.
   * @param {number|string} sectionIndex  0 = lead/intro, 1+ = numbered sections
   * @returns {Promise<string>}
   */
  function getSectionWikitext(sectionIndex) {
    return mwApi.get({
      action: 'parse',
      page: PAGE,
      section: sectionIndex,
      prop: 'wikitext',
      format: 'json',
      formatversion: 2,
    }).then(function (d) { return d.parse.wikitext; });
  }

  /**
   * Converts wikitext → HTML via the Parsoid REST API.
   * body_only=true strips the outer <html>/<head> wrapper so we get
   * clean HTML suitable for contentEditable (same approach nicEdit uses
   * for its iframe mode, but without needing an iframe).
   * @param {string} wikitext
   * @returns {Promise<string>} HTML string
   */
  function wikitextToHtml(wikitext) {
    return fetch(REST + '/wikitext/to/html/' + encodeURIComponent(PAGE), {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({ wikitext: wikitext, body_only: 'true' }),
    }).then(function (r) {
      if (!r.ok) throw new Error('Parsoid wikitext→html: HTTP ' + r.status);
      return r.text();
    });
  }

  /**
   * Converts HTML → wikitext via the Parsoid REST API.
   * scrub_wikitext=1 cleans up Parsoid-specific HTML attributes before conversion,
   * reducing noise from browser-edited markup.
   * @param {string} html
   * @returns {Promise<string>} wikitext string
   */
  function htmlToWikitext(html) {
    return fetch(REST + '/html/to/wikitext/' + encodeURIComponent(PAGE), {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({ html: html, scrub_wikitext: '1' }),
    }).then(function (r) {
      if (!r.ok) throw new Error('Parsoid html→wikitext: HTTP ' + r.status);
      return r.text();
    });
  }

  /**
   * Saves a section's wikitext via the MediaWiki edit API.
   * postWithToken automatically fetches and attaches the CSRF token.
   * @param {number|string} sectionIndex
   * @param {string} wikitext
   * @returns {Promise<Object>} API response
   */
  function saveSection(sectionIndex, wikitext) {
    return mwApi.postWithToken('csrf', {
      action: 'edit',
      title: PAGE,
      section: sectionIndex,
      text: wikitext,
      summary: 'Visual mobile edit ([[User:' + USER + '/WikiEditMobile.js|WikiEditMobile]])',
      format: 'json',
    });
  }

  // ──────────────────────────────────────────────────────────────────────────────
  // MobileEditor class — nicEdit-inspired contentEditable wrapper
  //
  // nicEdit wraps a div or textarea in a contentEditable surface and dispatches
  // execCommand calls from toolbar buttons. We follow the same pattern but
  // target mobile: a horizontally-scrolling toolbar with 36×36px touch targets,
  // no image sprites (Unicode labels instead), and touch event handling.
  // ──────────────────────────────────────────────────────────────────────────────

  function MobileEditor(toolbarEl, editableEl) {
    this.toolbar  = toolbarEl;
    this.editable = editableEl;
    this._activeStateTimer = null;

    this._buildToolbar();
    this.editable.contentEditable = 'true';
    this.editable.addEventListener('keydown', this._onKeyDown.bind(this));

    // Update button active states when selection changes (like nicEditorButton.checkNodes)
    this.editable.addEventListener('keyup',   this._updateActiveStates.bind(this));
    this.editable.addEventListener('mouseup', this._updateActiveStates.bind(this));
    // selectionchange fires on mobile tap-selection
    document.addEventListener('selectionchange', this._updateActiveStates.bind(this));
  }

  MobileEditor.prototype._buildToolbar = function () {
    var self = this;
    this.toolbar.innerHTML = '';
    this._btnEls = {};   // cmd → button element, for active-state tracking

    BUTTONS.forEach(function (def) {
      if (def.sep) {
        var sep = document.createElement('div');
        sep.className = 'wem-sep';
        self.toolbar.appendChild(sep);
        return;
      }

      var btn = document.createElement('button');
      btn.className = 'wem-btn';
      btn.type      = 'button';
      btn.title     = def.title || '';
      btn.innerHTML = def.label;
      if (def.style) { btn.style.cssText = def.style; }

      // Prevent blur on the editable when tapping toolbar buttons.
      // nicEditorButton.construct does the same with bkLib.cancelEvent on mousedown.
      btn.addEventListener('mousedown', function (e) {
        e.preventDefault();
        self._exec(def);
      });
      // Touch devices fire touchstart; we preventDefault to avoid the 300ms delay
      // and the subsequent mousedown that would double-fire.
      btn.addEventListener('touchstart', function (e) {
        e.preventDefault();
        self._exec(def);
      }, { passive: false });

      self.toolbar.appendChild(btn);
      if (def.cmd) { self._btnEls[def.cmd] = btn; }
    });
  };

  /**
   * Save the current selection so it can be restored after a dialog closes.
   * Mirrors nicEditorInstance.saveRng / restoreRng.
   */
  MobileEditor.prototype._saveSelection = function () {
    var sel = window.getSelection();
    this._savedRange = (sel && sel.rangeCount > 0) ? sel.getRangeAt(0).cloneRange() : null;
  };

  MobileEditor.prototype._restoreSelection = function () {
    if (!this._savedRange) return;
    var sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(this._savedRange);
  };

  /**
   * Execute a formatting command.
   * Mirrors nicEditorButton.mouseClick → ne.nicCommand → execCommand.
   * For special buttons (link, media, table) save the selection first,
   * then open a dialog pane — mirrors nicEditorAdvancedButton.mouseClick.
   */
  MobileEditor.prototype._exec = function (def) {
    if (def.special) {
      // Save selection before the dialog steals focus (nicEdit: saveRng)
      this._saveSelection();
      if (def.cmd === 'createLink')  { showLinkDialog(this);  return; }
      if (def.cmd === 'insertMedia') { showMediaDialog(this); return; }
      if (def.cmd === 'insertTable') { showTableDialog(this); return; }
      return;
    }

    this.editable.focus();
    if (def.val) {
      document.execCommand(def.cmd, false, def.val);
    } else {
      document.execCommand(def.cmd, false, null);
    }
    this._updateActiveStates();
  };

  /**
   * Keyboard shortcuts — mirrors nicEditorButton.key which listens for Ctrl+key.
   */
  MobileEditor.prototype._onKeyDown = function (e) {
    if (e.ctrlKey || e.metaKey) {
      var cmd = KEY_MAP[e.key.toLowerCase()];
      if (cmd) {
        e.preventDefault();
        document.execCommand(cmd, false, null);
        this._updateActiveStates();
      }
    }
  };

  /**
   * Reflect the active formatting state on toolbar buttons.
   * nicEditorButton.checkNodes does the same by walking up the DOM tree
   * to see if the cursor is inside a relevant tag.
   */
  MobileEditor.prototype._updateActiveStates = function () {
    var self = this;
    // Debounce to avoid firing on every selectionchange pixel
    clearTimeout(this._activeStateTimer);
    this._activeStateTimer = setTimeout(function () {
      ['bold', 'italic', 'insertOrderedList', 'insertUnorderedList'].forEach(function (cmd) {
        var el = self._btnEls[cmd];
        if (!el) return;
        try {
          if (document.queryCommandState(cmd)) {
            el.classList.add('wem-active');
          } else {
            el.classList.remove('wem-active');
          }
        } catch (ex) { /* queryCommandState not supported for this cmd */ }
      });
    }, 50);
  };

  MobileEditor.prototype.getContent = function () {
    return this.editable.innerHTML;
  };

  MobileEditor.prototype.setContent = function (html) {
    this.editable.innerHTML = html;
  };

  MobileEditor.prototype.focus = function () {
    // Small delay so the overlay animation finishes first on mobile
    var el = this.editable;
    setTimeout(function () { el.focus(); }, 80);
  };

  // ──────────────────────────────────────────────────────────────────────────────
  // Dialog system
  // Mirrors nicEdit's nicEditorPane + nicEditorAdvancedButton.addForm:
  // a floating panel that collects input, restores the saved selection, then
  // calls execCommand or insertHTML to modify the editable content.
  // ──────────────────────────────────────────────────────────────────────────────

  var activeEditorRef = null; // editor instance that opened the current dialog

  /**
   * Build and show a bottom-sheet dialog.
   * @param {string}   title     - Dialog heading
   * @param {string}   insertLabel - Text for the insert button
   * @param {Function} buildBody - Called with the dialog body element; should
   *                               append .wem-field rows and return a function
   *                               that returns the collected values (or null to cancel).
   * @param {Function} onInsert  - Called with the collected values when user confirms.
   */
  function openDialog(title, insertLabel, buildBody, onInsert) {
    var backdrop = document.getElementById('wem-dialog-backdrop');
    backdrop.innerHTML = '';
    backdrop.hidden = false;

    var dlg = ce('div', { id: 'wem-dialog', role: 'dialog', 'aria-modal': 'true' });

    var ttl = ce('p', { id: 'wem-dialog-title' });
    ttl.textContent = title;

    var body   = ce('div', { id: 'wem-dialog-body' });
    var footer = ce('div', { id: 'wem-dialog-footer' });

    var cancelBtn = ce('button', { id: 'wem-dlg-cancel', type: 'button' });
    cancelBtn.textContent = 'Cancel';

    var insertBtn = ce('button', { id: 'wem-dlg-insert', type: 'button' });
    insertBtn.textContent = insertLabel;

    footer.appendChild(cancelBtn);
    footer.appendChild(insertBtn);
    dlg.appendChild(ttl);
    dlg.appendChild(body);
    dlg.appendChild(footer);
    backdrop.appendChild(dlg);

    // buildBody populates the form and returns a collect() function
    var collect = buildBody(body);

    function close() {
      backdrop.hidden = true;
      backdrop.innerHTML = '';
    }

    cancelBtn.addEventListener('click', function () {
      close();
      if (activeEditorRef) { activeEditorRef._restoreSelection(); }
    });

    insertBtn.addEventListener('click', function () {
      var values = collect();
      if (!values) return; // validation failed inside collect()
      close();
      if (activeEditorRef) {
        activeEditorRef._restoreSelection();
        activeEditorRef.editable.focus();
      }
      onInsert(values);
    });

    // Tap outside dialog closes it
    backdrop.addEventListener('click', function (e) {
      if (e.target === backdrop) {
        close();
        if (activeEditorRef) { activeEditorRef._restoreSelection(); }
      }
    });

    // Focus first input
    setTimeout(function () {
      var first = dlg.querySelector('input, select');
      if (first) { first.focus(); }
    }, 50);
  }

  function closeDialog() {
    var backdrop = document.getElementById('wem-dialog-backdrop');
    if (backdrop) { backdrop.hidden = true; backdrop.innerHTML = ''; }
  }

  // ── Field helpers (mirrors nicEditorAdvancedButton.addForm) ──────────────────

  function makeField(id, labelTxt, inputEl) {
    var wrap  = ce('div', { 'class': 'wem-field' });
    var label = ce('label');
    label.setAttribute('for', id);
    label.textContent = labelTxt;
    inputEl.id = id;
    wrap.appendChild(label);
    wrap.appendChild(inputEl);
    return wrap;
  }

  function textInput(placeholder, value) {
    var el = ce('input', { type: 'text' });
    el.placeholder = placeholder || '';
    el.value       = value || '';
    return el;
  }

  function selectInput(options, selectedVal) {
    var el = ce('select');
    options.forEach(function (opt) {
      var o = ce('option', { value: opt[0] });
      o.textContent = opt[1];
      if (opt[0] === selectedVal) { o.selected = true; }
      el.appendChild(o);
    });
    return el;
  }

  function numberInput(min, max, val) {
    var el = ce('input', { type: 'number' });
    el.min   = String(min);
    el.max   = String(max);
    el.value = String(val);
    el.style.cssText = 'width:64px';
    return el;
  }

  // ── Link dialog ──────────────────────────────────────────────────────────────

  function showLinkDialog(edRef) {
    activeEditorRef = edRef;

    // Pre-fill href if cursor is already inside an <a> — mirrors nicLinkButton
    var sel  = window.getSelection();
    var node = sel && sel.anchorNode;
    var existingA = null;
    while (node && node !== edRef.editable) {
      if (node.nodeName === 'A') { existingA = node; break; }
      node = node.parentNode;
    }

    openDialog('Insert / Edit Link', 'Insert link',
      function (body) {
        var hrefEl  = textInput('https://…', (existingA && existingA.href) || 'https://');
        var textEl  = textInput('Display text (optional)',
                                existingA ? (existingA.textContent || '') : '');
        var targetEl = selectInput(
          [['', 'Same window'], ['_blank', 'New tab']],
          (existingA && existingA.target) || ''
        );

        body.appendChild(makeField('dlg-href',   'URL',          hrefEl));
        body.appendChild(makeField('dlg-ltext',  'Link text',    textEl));
        body.appendChild(makeField('dlg-target', 'Opens in',     targetEl));

        return function collect() {
          var href = hrefEl.value.trim();
          if (!href || href === 'https://') {
            hrefEl.focus();
            hrefEl.style.borderColor = '#d33';
            return null;
          }
          return { href: href, text: textEl.value.trim(), target: targetEl.value };
        };
      },
      function onInsert(v) {
        if (existingA) {
          // Update existing link in-place
          existingA.href   = v.href;
          existingA.target = v.target;
          if (v.text) { existingA.textContent = v.text; }
        } else {
          // If the user typed display text, select it first so execCommand wraps it
          if (v.text) {
            document.execCommand('insertText', false, v.text);
            // Re-select the just-inserted text
            var s = window.getSelection();
            var r = s.getRangeAt(0);
            r.setStart(r.startContainer, r.startOffset - v.text.length);
            s.removeAllRanges();
            s.addRange(r);
          }
          document.execCommand('createLink', false, v.href);
          // Set target attribute on the newly created <a>
          if (v.target) {
            var links = edRef.editable.querySelectorAll('a[href="' + v.href + '"]');
            links.forEach(function (a) { a.target = v.target; });
          }
        }
      }
    );
  }

  // ── Media (image) dialog ─────────────────────────────────────────────────────
  // Inserts a Parsoid-compatible <figure> element.
  // Parsoid's html→wikitext converter recognises this pattern and produces
  // [[File:Name.jpg|type|align|widthpx|Caption]] syntax.

  function showMediaDialog(edRef) {
    activeEditorRef = edRef;

    openDialog('Insert Image', 'Insert image',
      function (body) {
        var fileEl    = textInput('Example.jpg', '');
        var captionEl = textInput('Image caption', '');
        var typeEl    = selectInput(
          [['Thumb', 'Thumbnail'], ['Frame', 'Framed'], ['Frameless', 'Frameless'], ['', 'Plain']],
          'Thumb'
        );
        var alignEl   = selectInput(
          [['', 'Default'], ['right', 'Right'], ['left', 'Left'], ['center', 'Center'], ['none', 'None']],
          'right'
        );
        var widthEl   = numberInput(50, 1200, 220);

        body.appendChild(makeField('dlg-file',    'File name (no "File:" prefix)', fileEl));
        body.appendChild(makeField('dlg-caption', 'Caption',    captionEl));

        // Type + Align on one row
        var row1 = ce('div', { 'class': 'wem-field-row' });
        row1.appendChild(makeField('dlg-type',  'Type',      typeEl));
        row1.appendChild(makeField('dlg-align', 'Alignment', alignEl));
        body.appendChild(row1);

        // Width on its own row
        var row2 = ce('div', { 'class': 'wem-field-row' });
        var wfWrap = ce('div', { 'class': 'wem-field narrow' });
        var wLabel = ce('label'); wLabel.setAttribute('for', 'dlg-width');
        wLabel.textContent = 'Width (px)';
        widthEl.id = 'dlg-width';
        wfWrap.appendChild(wLabel); wfWrap.appendChild(widthEl);
        row2.appendChild(wfWrap);
        body.appendChild(row2);

        return function collect() {
          var fname = fileEl.value.trim().replace(/^File:/i, '');
          if (!fname) {
            fileEl.focus(); fileEl.style.borderColor = '#d33';
            return null;
          }
          return {
            file:    fname,
            caption: captionEl.value.trim(),
            type:    typeEl.value,
            align:   alignEl.value,
            width:   parseInt(widthEl.value, 10) || 220,
          };
        };
      },
      function onInsert(v) {
        // Build a Parsoid-style <figure> that round-trips cleanly via html→wikitext
        var typeofAttr = 'mw:File' + (v.type ? '/' + v.type : '');
        var alignClass = v.align ? ' mw-halign-' + v.align : '';
        var href       = './File:' + encodeURIComponent(v.file);
        var html = '<figure typeof="' + typeofAttr + '" class="mw-default-size' + alignClass + '">' +
                   '<a href="' + href + '">' +
                   '<img resource="' + href + '" src="" alt="' + escHtml(v.file) +
                   '" width="' + v.width + '"></a>' +
                   (v.caption ? '<figcaption>' + escHtml(v.caption) + '</figcaption>' : '') +
                   '</figure><p></p>';
        document.execCommand('insertHTML', false, html);
      }
    );
  }

  // ── Table dialog ─────────────────────────────────────────────────────────────

  function showTableDialog(edRef) {
    activeEditorRef = edRef;

    openDialog('Insert Table', 'Insert table',
      function (body) {
        var rowsEl  = numberInput(1, 50, 3);
        var colsEl  = numberInput(1, 20, 3);
        var hdrEl   = selectInput(
          [['yes', 'Yes — first row as header'],
           ['no',  'No header row']],
          'yes'
        );

        var sizeRow = ce('div', { 'class': 'wem-field-row' });

        var rWrap = ce('div', { 'class': 'wem-field' });
        var rLbl  = ce('label'); rLbl.setAttribute('for', 'dlg-rows');
        rLbl.textContent = 'Rows'; rowsEl.id = 'dlg-rows';
        rWrap.appendChild(rLbl); rWrap.appendChild(rowsEl);

        var cWrap = ce('div', { 'class': 'wem-field' });
        var cLbl  = ce('label'); cLbl.setAttribute('for', 'dlg-cols');
        cLbl.textContent = 'Columns'; colsEl.id = 'dlg-cols';
        cWrap.appendChild(cLbl); cWrap.appendChild(colsEl);

        sizeRow.appendChild(rWrap);
        sizeRow.appendChild(cWrap);
        body.appendChild(sizeRow);
        body.appendChild(makeField('dlg-hdr', 'Header row', hdrEl));

        return function collect() {
          return {
            rows:   Math.max(1, parseInt(rowsEl.value, 10) || 3),
            cols:   Math.max(1, parseInt(colsEl.value, 10) || 3),
            header: hdrEl.value === 'yes',
          };
        };
      },
      function onInsert(v) {
        var html = '<table style="border-collapse:collapse;width:100%"><tbody>';
        for (var r = 0; r < v.rows; r++) {
          html += '<tr>';
          for (var c = 0; c < v.cols; c++) {
            if (r === 0 && v.header) {
              html += '<th style="border:1px solid #a2a9b1;padding:6px 8px;background:#eaecf0">Header ' + (c + 1) + '</th>';
            } else {
              html += '<td style="border:1px solid #a2a9b1;padding:6px 8px"> </td>';
            }
          }
          html += '</tr>';
        }
        html += '</tbody></table><p></p>';
        document.execCommand('insertHTML', false, html);
      }
    );
  }

  // ── HTML escape helper ───────────────────────────────────────────────────────

  function escHtml(s) {
    return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  }

  // ──────────────────────────────────────────────────────────────────────────────
  // UI
  // ──────────────────────────────────────────────────────────────────────────────

  var editor     = null;  // MobileEditor instance
  var curSection = null;  // currently open section descriptor
  var curView    = 'sections'; // 'sections' | 'editor'

  function setStatus(msg, type) {
    var el = document.getElementById('wem-status');
    el.textContent = msg;
    el.className   = type === 'err' ? 'wem-err' : type === 'ok' ? 'wem-ok' : '';
  }

  function showSectionsView() {
    curView    = 'sections';
    curSection = null;
    document.getElementById('wem-sections').hidden  = false;
    document.getElementById('wem-editor').hidden    = true;
    document.getElementById('wem-toolbar').hidden   = true;   // hide when browsing sections
    document.getElementById('wem-hdr-title').textContent = 'Edit article';
    document.getElementById('wem-save').disabled   = true;
    document.getElementById('wem-back').style.visibility = 'hidden';
    setStatus('');
    if (editor) { editor.setContent(''); }
  }

  function showEditorView(section) {
    curView    = 'editor';
    curSection = section;

    document.getElementById('wem-sections').hidden = true;
    document.getElementById('wem-editor').hidden   = false;
    document.getElementById('wem-toolbar').hidden  = false;   // show at bottom above keyboard
    document.getElementById('wem-hdr-title').textContent = section.line || 'Introduction';
    document.getElementById('wem-back').style.visibility = 'visible';
    document.getElementById('wem-save').disabled  = true;
    setStatus('Fetching section wikitext…');

    getSectionWikitext(section.index)
      .then(function (wt) {
        setStatus('Converting to HTML via Parsoid…');
        return wikitextToHtml(wt);
      })
      .then(function (html) {
        editor.setContent(html);
        editor.focus();
        document.getElementById('wem-save').disabled = false;
        setStatus('Ready — edit freely, then tap Save');
      })
      ['catch'](function (err) {
        setStatus('⚠ ' + err.message, 'err');
      });
  }

  function handleSave() {
    if (!curSection || !editor) return;

    var saveBtn = document.getElementById('wem-save');
    saveBtn.disabled = true;
    setStatus('Converting back to wikitext…');

    var html = editor.getContent();

    htmlToWikitext(html)
      .then(function (wt) {
        setStatus('Saving…');
        return saveSection(curSection.index, wt);
      })
      .then(function (res) {
        if (res.edit && res.edit.result === 'Success') {
          setStatus('✓ Saved! Reloading…', 'ok');
          setTimeout(function () { location.reload(); }, 1400);
        } else {
          // API returned an error object
          var errMsg = (res.error && res.error.info) ? res.error.info : JSON.stringify(res);
          throw new Error(errMsg);
        }
      })
      ['catch'](function (err) {
        setStatus('⚠ Save failed: ' + err.message, 'err');
        saveBtn.disabled = false;
      });
  }

  // ── DOM builders ─────────────────────────────────────────────────────────────

  function buildOverlay() {
    // ── Header ──
    var hdr  = ce('div', { id: 'wem-hdr' });
    var back = ce('button', { id: 'wem-back', 'aria-label': 'Back' });
    back.textContent = '←';

    var titleEl = ce('span', { id: 'wem-hdr-title' });
    titleEl.textContent = 'Edit article';

    var saveBtn = ce('button', { id: 'wem-save' });
    saveBtn.textContent = 'Save';
    saveBtn.disabled = true;

    hdr.appendChild(back);
    hdr.appendChild(titleEl);
    hdr.appendChild(saveBtn);

    // ── Section list view ──
    var sectionsDiv = ce('div', { id: 'wem-sections' });

    // ── Editor view (content only — toolbar lives outside this pane) ──
    var editorPane    = ce('div', { id: 'wem-editor' });
    editorPane.hidden = true;

    var editWrap    = ce('div', { id: 'wem-editable-wrap' });
    var editableDiv = ce('div', { id: 'wem-editable' });
    editableDiv.setAttribute('role', 'textbox');
    editableDiv.setAttribute('aria-multiline', 'true');
    editableDiv.setAttribute('aria-label', 'Section content');
    editWrap.appendChild(editableDiv);
    editorPane.appendChild(editWrap);

    // ── Status bar (between content area and toolbar) ──
    var status = ce('div', { id: 'wem-status' });

    // ── Toolbar — bottom of overlay, appears just above the soft keyboard.
    //    Starts hidden; shown only when the editor view is active. ──
    var toolbar = ce('div', { id: 'wem-toolbar' });
    toolbar.hidden = true;

    // ── Dialog backdrop (position:absolute inside overlay covers everything
    //    including the toolbar when a dialog is open) ──
    var backdrop = ce('div', { id: 'wem-dialog-backdrop' });
    backdrop.hidden = true;

    // ── Overlay — stacking order (top → bottom on screen):
    //    header · sections/editor · status · toolbar · dialog backdrop ──
    var overlay = ce('div', { id: 'wem-overlay' });
    overlay.hidden = true;

    overlay.appendChild(hdr);
    overlay.appendChild(sectionsDiv);
    overlay.appendChild(editorPane);
    overlay.appendChild(status);
    overlay.appendChild(toolbar);    // ← bottom of screen, above keyboard
    overlay.appendChild(backdrop);
    document.body.appendChild(overlay);

    // ── Wire up editor ──
    editor = new MobileEditor(toolbar, editableDiv);

    // ── Events ──
    back.addEventListener('click', function () {
      // If a dialog is open, close it first (like nicEditorAdvancedButton.removePane)
      if (!backdrop.hidden) { closeDialog(); return; }
      if (curView === 'editor') {
        showSectionsView();
      } else {
        overlay.hidden = true;
      }
    });

    saveBtn.addEventListener('click', handleSave);

    // Hardware back / Escape key
    document.addEventListener('keydown', function (e) {
      if (e.key === 'Escape' && !overlay.hidden) {
        if (curView === 'editor') { showSectionsView(); }
        else                      { overlay.hidden = true; }
      }
    });

    return { overlay: overlay, sectionsEl: sectionsDiv };
  }

  function makeSectionItem(section) {
    var item = ce('div', { 'class': 'wem-sec' });
    // Indent based on heading level (like nicEdit's reorder)
    item.style.paddingLeft = (16 + Math.max(0, (section.toclevel - 1) * 12)) + 'px';

    var lv  = ce('span', { 'class': 'wem-sec-lv' });
    lv.textContent = 'H' + (section.toclevel + 1);

    var nm  = ce('span', { 'class': 'wem-sec-nm' });
    nm.textContent = section.line;

    var arr = ce('span', { 'class': 'wem-sec-arr' });
    arr.textContent = '›';

    item.appendChild(lv);
    item.appendChild(nm);
    item.appendChild(arr);

    item.addEventListener('click', function () { showEditorView(section); });
    return item;
  }

  // Tiny element factory (mirrors nicEdit's bkElement pattern)
  function ce(tag, attrs) {
    var el = document.createElement(tag);
    if (attrs) {
      Object.keys(attrs).forEach(function (k) {
        if (k === 'class') { el.className = attrs[k]; }
        else               { el.setAttribute(k, attrs[k]); }
      });
    }
    return el;
  }

  // ── Bootstrap ─────────────────────────────────────────────────────────────────

  function init() {
    if (document.getElementById('wem-fab')) return; // guard against double-init

    // Inject styles
    var style = document.createElement('style');
    style.textContent = CSS;
    document.head.appendChild(style);

    // Build the overlay (hidden until FAB is tapped)
    var ui = buildOverlay();
    showSectionsView();

    // Floating action button — pencil icon, fixed bottom-right
    var fab = ce('button', { id: 'wem-fab', title: 'Visual section editor', 'aria-label': 'Visual section editor' });
    fab.innerHTML = '&#9998;'; // ✎
    document.body.appendChild(fab);

    fab.addEventListener('click', function () {
      ui.overlay.hidden = false;
      showSectionsView();
      setStatus('Loading sections…');

      getSections()
        .then(function (sections) {
          var container = ui.sectionsEl;
          container.innerHTML = '';

          // Section 0 = lead / introduction (before the first heading)
          container.appendChild(
            makeSectionItem({ index: 0, line: 'Introduction', toclevel: 1 })
          );

          sections.forEach(function (s) {
            if (s.line) {
              container.appendChild(makeSectionItem(s));
            }
          });

          setStatus('Tap a section to begin editing');
        })
        ['catch'](function (err) {
          setStatus('⚠ Could not load sections: ' + err.message, 'err');
        });
    });
  }

  // mw.hook fires after the page content is ready, including after
  // Minerva's ajax-navigation loads a new page — same pattern nicEdit
  // recommends via bkLib.onDomLoaded.
  mw.hook('wikipage.content').add(init);

}());
"https://schoolwiki.in/index.php?title=ഉപയോക്താവ്:Ranjithsiji/WikiEditMobile.js&oldid=2989161" എന്ന താളിൽനിന്ന് ശേഖരിച്ചത്