|
|
| Line 459: |
Line 459: |
| if (window.mw && mw.hook) mw.hook('wikipage.content').add(initFullSliders); | | if (window.mw && mw.hook) mw.hook('wikipage.content').add(initFullSliders); |
| })(); | | })(); |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
| // --- Disable Tools Option for all users except admin ---
| |
| /* ============================================================
| |
| Hide Tools menu for all non-admin users (Chameleon skin)
| |
| Show Tools normally for sysop users.
| |
| 100% reliable version
| |
| ============================================================ */
| |
|
| |
| mw.loader.using(['mediawiki.user'], function () {
| |
|
| |
| mw.user.getGroups().then(function (groups) {
| |
|
| |
| var isAdmin = groups.indexOf('sysop') !== -1;
| |
|
| |
| // Add CSS class to <body> for non-admin
| |
| if (!isAdmin) {
| |
| document.documentElement.classList.add('hide-tools');
| |
| return;
| |
| }
| |
|
| |
| // Admin: ensure Tools menu is visible
| |
| // Chameleon sometimes loads menu late → use multiple attempts
| |
| function showTools() {
| |
|
| |
| // Remove hide class completely
| |
| document.documentElement.classList.remove('hide-tools');
| |
|
| |
| // Explicitly show Tools containers
| |
| var dropdown = document.querySelector('.p-tb-dropdown');
| |
| if (dropdown) dropdown.style.display = '';
| |
|
| |
| var toggleLink = document.querySelector('.p-tb-toggle');
| |
| if (toggleLink) toggleLink.parentElement.style.display = '';
| |
| }
| |
|
| |
| // Try repeatedly to bypass lazy loading
| |
| showTools();
| |
| setTimeout(showTools, 200);
| |
| setTimeout(showTools, 600);
| |
| setTimeout(showTools, 1000);
| |
| setTimeout(showTools, 1500);
| |
|
| |
| });
| |
|
| |
| });
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
| // == Showing Template as default footer
| |
| $(function () {
| |
|
| |
| // Hindi detection
| |
| var parts = window.location.pathname.split('/').filter(Boolean);
| |
| var isHindi = parts.length > 0 && parts[0].toLowerCase() === 'hi';
| |
| var template = isHindi ? 'Custom-footer-Hindi' : 'Custom-footer';
| |
|
| |
| // Watcher for footer DOM creation
| |
| const observer = new MutationObserver(function () {
| |
|
| |
| // When the footer block is PRESENT and FULLY BUILT
| |
| if ($('.footercontainer.container #footer-info').length &&
| |
| $('.footercontainer.container #footer-places').length) {
| |
|
| |
| // Remove default footer, icons, extra rows
| |
| $('.footercontainer.container').remove();
| |
|
| |
| // Add your custom footer AFTER Chameleon block
| |
| $.ajax({
| |
| url: '/index.php?title=Template:' + template + '&action=render',
| |
| type: 'GET',
| |
| success: function (data) {
| |
|
| |
| // Avoid duplicates
| |
| if (!$('#mw-custom-footer').length) {
| |
| $('<div id="mw-custom-footer"></div>')
| |
| .html(data)
| |
| .appendTo('body');
| |
| }
| |
| }
| |
| });
| |
|
| |
| // Stop watching
| |
| observer.disconnect();
| |
| }
| |
| });
| |
|
| |
| // Now actually watch the body for late additions
| |
| observer.observe(document.body, {
| |
| childList: true,
| |
| subtree: true
| |
| });
| |
|
| |
| });
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
| // Related_links button mobile
| |
|
| |
| $(document).ready(function () {
| |
| var box = $('.related-links-box');
| |
|
| |
| if (box.length) {
| |
| $('body').on('click', '.related-links-box h2', function () {
| |
| box.toggleClass('open');
| |
| });
| |
| }
| |
| });
| |
|
| |
|
| |
| //Category list show more button
| |
| $(document).ready(function () {
| |
| $('.show-all-categories').on('click', function () {
| |
| $('.category-full-list').slideDown(200);
| |
| $(this).hide();
| |
| });
| |
| });
| |
|
| |
|
| |
|
| |
|
| |
| // Latest article Creation UI
| |
|
| |
| $(document).ready(function () {
| |
|
| |
| // Only run on this page
| |
| if (mw.config.get("wgPageName") !== "LatestArticlesUpdate") return;
| |
|
| |
| console.log("Latest Articles Editor Loaded (ES5)");
| |
|
| |
| // Load JSON from page
| |
| new mw.Api().get({
| |
| action: "query",
| |
| titles: "LatestArticlesJSON",
| |
| prop: "revisions",
| |
| rvprop: "content",
| |
| formatversion: "2"
| |
| }).done(function (res) {
| |
|
| |
| var content = res.query.pages[0].revisions[0].content || "[]";
| |
| var data = [];
| |
|
| |
| try {
| |
| data = JSON.parse(content);
| |
| } catch (e) {}
| |
|
| |
| // Prefill boxes
| |
| for (var i = 0; i < data.length; i++) {
| |
| $("#title" + (i + 1)).val(data[i].title);
| |
| $("#link" + (i + 1)).val(data[i].link);
| |
| }
| |
|
| |
| });
| |
|
| |
|
| |
| // SAVE BUTTON CLICK
| |
| $("#saveLatestArticlesBtn").click(function () {
| |
|
| |
| var items = [];
| |
| var i, t, l;
| |
|
| |
| for (i = 1; i <= 10; i++) {
| |
|
| |
| t = $("#title" + i).val().trim();
| |
| l = $("#link" + i).val().trim();
| |
|
| |
| if (t !== "" && l !== "") {
| |
| items.push({ title: t, link: l });
| |
| }
| |
| }
| |
|
| |
| // Save JSON back to storage page
| |
| new mw.Api().postWithEditToken({
| |
| action: "edit",
| |
| title: "LatestArticlesJSON",
| |
| text: JSON.stringify(items, null, 2),
| |
| summary: "Updated Latest Articles"
| |
| }).done(function () {
| |
| $("#saveStatus").html("<b style='color:green;'>✔ Saved successfully!</b>");
| |
| });
| |
|
| |
| });
| |
|
| |
| });
| |
|
| |
|
| |
|
| |
| // To show updated articles in Frontpage
| |
|
| |
| mw.hook("wikipage.content").add(function () {
| |
|
| |
| // Run only if div exists
| |
| if (!document.getElementById("LatestArticlesOutput")) return;
| |
|
| |
| console.log("Homepage Latest Articles Loader Running");
| |
|
| |
| new mw.Api().get({
| |
| action: "query",
| |
| titles: "LatestArticlesJSON",
| |
| prop: "revisions",
| |
| rvprop: "content",
| |
| formatversion: "2"
| |
| }).done(function (res) {
| |
|
| |
| var content = res.query.pages[0].revisions[0].content || "[]";
| |
| var data = [];
| |
|
| |
| try { data = JSON.parse(content); } catch(e){}
| |
|
| |
| var out = $("#LatestArticlesOutput");
| |
| out.html("");
| |
|
| |
| for (var i = 0; i < data.length; i++) {
| |
| var row = data[i];
| |
| out.append(
| |
| '<a class="latestArticleBtn" href="' + row.link + '">' +
| |
| row.title +
| |
| '</a>'
| |
| );
| |
| }
| |
| });
| |
|
| |
| });
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
| // Paste this into MediaWiki:Common.js
| |
| mw.loader.using('ext.visualEditor.core').then(function () {
| |
|
| |
| // Small helper: detect domain-like text (gives true for "example.com", "sub.example.co", "example.com/path")
| |
| function looksLikeDomain(text) {
| |
| if (!text) return false;
| |
| // allow www., allow path after TLD, basic heuristic
| |
| return /\b([a-z0-9-]+\.)+[a-z]{2,}(\/\S*)?/i.test(text.trim());
| |
| }
| |
|
| |
| // Normalize a domain-like text into an https:// URL
| |
| function buildExternalFromText(text) {
| |
| text = (text || '').trim();
| |
| if (!text) return null;
| |
| // If already has protocol, return as-is
| |
| if (/^\w+:\/\//.test(text)) return text;
| |
| // Remove leading slashes if VE added them (e.g. "/Writerpage1/gulfplacement.com")
| |
| text = text.replace(/^\/+/, '');
| |
| // If text contains ' ' (label + url), try to extract domain-like chunk
| |
| var match = text.match(/([a-z0-9-]+\.)+[a-z]{2,}(\/\S*)?/i);
| |
| if (match) {
| |
| return 'https://' + match[0];
| |
| }
| |
| return 'https://' + text;
| |
| }
| |
|
| |
| function ensureExternalAnchorsInSurface(surface) {
| |
| if (!surface || !surface.$element) return function () {};
| |
| var $el = surface.$element;
| |
| var observer = null;
| |
| var repeatTimer = null;
| |
| var repeatCount = 0;
| |
| var MAX_REPEAT = 30; // ~30 * 200ms = 6s of retries
| |
|
| |
| // fix anchors that VE rewrote into internal
| |
| function fixAnchorsOnce() {
| |
| try {
| |
| $el.find('a').each(function () {
| |
| var a = this;
| |
| var href = a.getAttribute('href') || '';
| |
| var text = a.textContent || a.innerText || '';
| |
| // If href is relative or missing protocol AND the visible link text looks like a domain
| |
| if (href && !/^\w+:\/\//.test(href) && looksLikeDomain(text)) {
| |
| var newHref = buildExternalFromText(text);
| |
| if (newHref) {
| |
| a.setAttribute('href', newHref);
| |
| // mark so we don't process again
| |
| a.setAttribute('data-external-forced', '1');
| |
| // show as external in VE dialog as well by adding rel
| |
| a.setAttribute('rel', 'noopener noreferrer');
| |
| }
| |
| }
| |
| // Some VE-created anchors may have href like "/Writerpage1/gulfplacement.com" or "Writerpage1/gulfplacement.com"
| |
| // The above logic catches those since href lacks protocol and text contains domain-like string.
| |
| });
| |
| } catch (e) {
| |
| // ignore
| |
| console.log('fixAnchorsOnce error', e);
| |
| }
| |
| }
| |
|
| |
| // Start a short-lived repetition (overrides background rewrite that runs after a delay)
| |
| function startRepeater() {
| |
| repeatCount = 0;
| |
| if (repeatTimer) {
| |
| clearInterval(repeatTimer);
| |
| }
| |
| repeatTimer = setInterval(function () {
| |
| fixAnchorsOnce();
| |
| repeatCount++;
| |
| if (repeatCount >= MAX_REPEAT) {
| |
| clearInterval(repeatTimer);
| |
| repeatTimer = null;
| |
| }
| |
| }, 200);
| |
| }
| |
|
| |
| // MutationObserver to catch transform events quickly
| |
| try {
| |
| observer = new MutationObserver(function () {
| |
| fixAnchorsOnce();
| |
| });
| |
| observer.observe($el[0], { childList: true, subtree: true, attributes: true });
| |
| } catch (e) {
| |
| // MutationObserver not available? fallback to simple interval already used above
| |
| console.log('MutationObserver not available', e);
| |
| }
| |
|
| |
| // return cleanup function (if needed)
| |
| return function cleanup() {
| |
| if (observer) {
| |
| try { observer.disconnect(); } catch (e) {}
| |
| observer = null;
| |
| }
| |
| if (repeatTimer) {
| |
| clearInterval(repeatTimer);
| |
| repeatTimer = null;
| |
| }
| |
| };
| |
| }
| |
|
| |
| // Wait for VE to initialize and then attach paste handler and anchor-fixer
| |
| ve.init.platform.addReadyCallback(function () {
| |
| try {
| |
| var surface = ve.init.target.getSurface();
| |
| if (!surface || !surface.$element) {
| |
| return;
| |
| }
| |
|
| |
| // Disable as many auto-internal-link hooks as reasonable client-side (best-effort)
| |
| try {
| |
| if (ve.ui && ve.ui.MWInternalLinkAnnotation) {
| |
| if (ve.ui.MWInternalLinkAnnotation.static) {
| |
| ve.ui.MWInternalLinkAnnotation.static.matchFunction = function () { return false; };
| |
| }
| |
| }
| |
| if (ve.dm && ve.dm.MWInternalLinkAnnotation && ve.dm.MWInternalLinkAnnotation.static) {
| |
| ve.dm.MWInternalLinkAnnotation.static.matchRdfaTypes = [];
| |
| ve.dm.MWInternalLinkAnnotation.static.matchFunction = function () { return false; };
| |
| }
| |
| if (ve.ce && ve.ce.MWInternalLinkAnnotation && ve.ce.MWInternalLinkAnnotation.static) {
| |
| ve.ce.MWInternalLinkAnnotation.static.matchRdfaTypes = [];
| |
| }
| |
| } catch (err) {
| |
| // ignore if internals differ
| |
| console.log('VE annotator override error', err);
| |
| }
| |
|
| |
| // Attach paste handler
| |
| surface.$element.on('paste', function (e) {
| |
| try {
| |
| var data = (e.originalEvent || e).clipboardData;
| |
| if (!data) return;
| |
| var text = data.getData('text/plain');
| |
| if (!text) return;
| |
|
| |
| // Prevent default paste (we will insert cleaned text)
| |
| e.preventDefault();
| |
|
| |
| // Convert any URLs to explicit external link syntax in the pasted text
| |
| // This helps VE treat them as external when inserted
| |
| text = text.replace(/https?:\/\/[^\s]+/g, function (url) {
| |
| return '[' + url + ']';
| |
| });
| |
|
| |
| // Insert cleaned text into VE surface model (best-effort simple insertion)
| |
| try {
| |
| // Use VE model insertion if available
| |
| if (surface.model && surface.model.getFragment) {
| |
| surface.model.getFragment().insertContent(text);
| |
| } else {
| |
| // fallback to inserting into contenteditable DOM
| |
| document.execCommand('insertText', false, text);
| |
| }
| |
| } catch (insertErr) {
| |
| // fallback DOM insert
| |
| document.execCommand('insertText', false, text);
| |
| }
| |
|
| |
| // Immediately run anchor fixer and start repeating to defeat delayed rewriter
| |
| var cleanup = ensureExternalAnchorsInSurface(surface);
| |
| // ensure cleanup after a while (we already limit repeats inside ensureExternalAnchorsInSurface)
| |
| setTimeout(function () {
| |
| if (typeof cleanup === 'function') {
| |
| try { cleanup(); } catch (e) {}
| |
| }
| |
| }, 8000); // give it up to 8s (safe upper bound) then cleanup
| |
| // also start immediate fight-back
| |
| // (the internal ensureExternalAnchorsInSurface already schedules repeats)
| |
| } catch (outerE) {
| |
| console.log('paste handler error', outerE);
| |
| }
| |
| });
| |
|
| |
| // Extra safety: run an initial pass on load to correct any existing bad anchors
| |
| try {
| |
| setTimeout(function () {
| |
| ensureExternalAnchorsInSurface(surface);
| |
| }, 500);
| |
| } catch (e) {
| |
| // ignore
| |
| }
| |
|
| |
| } catch (e) {
| |
| console.log('VE addReadyCallback error', e);
| |
| }
| |
| });
| |
|
| |
| });
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
| //Showing SEO button in Visual Editor
| |
| (function () {
| |
| mw.loader.using([
| |
| 'ext.visualEditor.desktopArticleTarget.init',
| |
| 'oojs-ui',
| |
| 'jquery',
| |
| 'mediawiki.api'
| |
| ]).done(function () {
| |
|
| |
| /* --------------------------------------------------
| |
| Add "SEO" button to VisualEditor toolbar
| |
| -------------------------------------------------- */
| |
| if (mw.config.get('wgIsArticle')) {
| |
| mw.hook('ve.activationComplete').add(function () {
| |
| try {
| |
| var veTarget = ve.init && ve.init.target;
| |
| var toolbar = veTarget && veTarget.toolbar;
| |
| if (!toolbar) return;
| |
| if (toolbar.$element.find('.ve-ui-toolbar-seoButton').length) return;
| |
|
| |
| var seoButton = new OO.ui.ButtonWidget({
| |
| label: 'SEO',
| |
| icon: 'settings',
| |
| classes: ['ve-ui-toolbar-seoButton']
| |
| });
| |
|
| |
| seoButton.on('click', function () {
| |
| openSEODialog(veTarget);
| |
| });
| |
|
| |
| toolbar.$element.find('.oo-ui-toolbar-actions').append(seoButton.$element);
| |
|
| |
| } catch (e) {
| |
| console.error('[SEO] Toolbar init error:', e);
| |
| }
| |
| });
| |
| }
| |
|
| |
|
| |
| /* --------------------------------------------------
| |
| SEO Dialog
| |
| -------------------------------------------------- */
| |
| function openSEODialog(veTarget) {
| |
|
| |
| var surface = veTarget.surface;
| |
| var model = surface.getModel();
| |
| var docText = '';
| |
|
| |
| try {
| |
| docText = model.getDocument().data.getText(true);
| |
| } catch (e) {
| |
| var txt = document.getElementById('wpTextbox1');
| |
| if (txt) docText = txt.value;
| |
| }
| |
|
| |
| // Extract existing SEO comment
| |
| var oldTitle = '', oldDesc = '', oldKeys = '';
| |
| var match = /<!--\s*SEO\s+title=(.*?)\s+description=([\s\S]*?)\s+keywords=([\s\S]*?)\s*-->/i.exec(docText);
| |
|
| |
| if (match) {
| |
| oldTitle = match[1].trim();
| |
| oldDesc = match[2].trim();
| |
| oldKeys = match[3].trim();
| |
| }
| |
|
| |
| // Input fields
| |
| var titleInput = new OO.ui.TextInputWidget({ placeholder: 'Meta Title', value: oldTitle });
| |
| var descInput = new OO.ui.MultilineTextInputWidget({ placeholder: 'Meta Description', rows: 3, value: oldDesc });
| |
| var keysInput = new OO.ui.TextInputWidget({ placeholder: 'Meta Keywords', value: oldKeys });
| |
|
| |
|
| |
| // Create Dialog
| |
| var SEODialog = function (cfg) { SEODialog.super.call(this, cfg); };
| |
| OO.inheritClass(SEODialog, OO.ui.ProcessDialog);
| |
|
| |
| SEODialog.static.name = 'seoDialog';
| |
| SEODialog.static.title = 'SEO Settings';
| |
| SEODialog.static.size = 'larger';
| |
| SEODialog.static.actions = [
| |
| { action: 'save', label: 'Save', flags: ['primary', 'progressive'] },
| |
| { action: 'cancel', label: 'Cancel', flags: ['safe'] }
| |
| ];
| |
|
| |
| SEODialog.prototype.initialize = function () {
| |
| SEODialog.super.prototype.initialize.apply(this, arguments);
| |
|
| |
| var panel = new OO.ui.PanelLayout({ padded: true, expanded: true });
| |
|
| |
| panel.$element.append(
| |
| $('<label>').text('Meta Title'),
| |
| titleInput.$element, $('<br><br>'),
| |
|
| |
| $('<label>').text('Meta Description'),
| |
| descInput.$element, $('<br><br>'),
| |
|
| |
| $('<label>').text('Meta Keywords'),
| |
| keysInput.$element
| |
| );
| |
|
| |
| this.$body.append(panel.$element);
| |
| };
| |
|
| |
| SEODialog.prototype.getBodyHeight = function () {
| |
| return 420;
| |
| };
| |
|
| |
|
| |
| /* --------------------------------------------------
| |
| FINAL FIX: SAFE SAVE USING API
| |
| (DO NOT CHANGE VE CONTENT)
| |
| -------------------------------------------------- */
| |
| SEODialog.prototype.getActionProcess = function (action) {
| |
|
| |
| var dialog = this;
| |
|
| |
| if (action === 'save') {
| |
| return new OO.ui.Process(function () {
| |
|
| |
| var t = titleInput.getValue().trim();
| |
| var d = descInput.getValue().trim();
| |
| var k = keysInput.getValue().trim();
| |
|
| |
| // New SEO block
| |
| var newSEO =
| |
| "<!--SEO title=\"" + mw.html.escape(t) +
| |
| "\" description=\"" + mw.html.escape(d) +
| |
| "\" keywords=\"" + mw.html.escape(k) +
| |
| "\" -->";
| |
|
| |
| var api = new mw.Api();
| |
|
| |
| // STEP 1: Load TRUE source wikitext (not VE text)
| |
| api.get({
| |
| action: "query",
| |
| prop: "revisions",
| |
| rvprop: "content",
| |
| formatversion: "2",
| |
| titles: mw.config.get("wgPageName")
| |
| }).done(function (data) {
| |
|
| |
| var page = data.query.pages[0];
| |
| var source = page.revisions[0].content;
| |
|
| |
| // STEP 2: Remove old SEO block
| |
| source = source.replace(/<!--SEO[\s\S]*?-->/i, "");
| |
|
| |
| // STEP 3: Add new SEO at very top
| |
| source = newSEO + "\n" + source;
| |
|
| |
| // STEP 4: Save back safely
| |
| api.postWithToken("csrf", {
| |
| action: "edit",
| |
| title: mw.config.get("wgPageName"),
| |
| text: source,
| |
| summary: "Updated SEO metadata"
| |
| }).done(function () {
| |
| mw.notify("✔ SEO updated successfully");
| |
| location.reload();
| |
| });
| |
| });
| |
|
| |
| dialog.close();
| |
| });
| |
| }
| |
|
| |
| if (action === 'cancel') {
| |
| return new OO.ui.Process(function () {
| |
| dialog.close();
| |
| });
| |
| }
| |
| };
| |
|
| |
|
| |
| // --------------------------------------------------
| |
| // Open dialog
| |
| // --------------------------------------------------
| |
| var wm = window._seoWM || new OO.ui.WindowManager();
| |
| if (!window._seoWM) {
| |
| window._seoWM = wm;
| |
| $(document.body).append(wm.$element);
| |
| }
| |
|
| |
| var dlg = new SEODialog();
| |
| wm.addWindows([dlg]);
| |
| wm.openWindow(dlg);
| |
| }
| |
|
| |
| });
| |
| })();
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
| /* ====================================================================
| |
| VISUALEDITOR – Writer Tools (Common.js Edition)
| |
| Word Count, Character Count, Reading Time
| |
| With Close/Open Button
| |
| ES5 Compatible | MediaWiki 1.39.x
| |
| ==================================================================== */
| |
|
| |
| (function () {
| |
|
| |
| if (window.WT_Loaded) return;
| |
| window.WT_Loaded = true;
| |
|
| |
| var PANEL_ID = 'wt-ve-panel';
| |
| var WPM = 200; // words per minute reading speed
| |
| var updateTimeout = null;
| |
| var surfaceRef = null;
| |
|
| |
| /* --------------------------------------------------------------
| |
| Create Writer Tools Panel + Open Button
| |
| -------------------------------------------------------------- */
| |
| function createPanel() {
| |
| if (document.getElementById(PANEL_ID)) return;
| |
|
| |
| /* Main Panel */
| |
| var panel = document.createElement('div');
| |
| panel.id = PANEL_ID;
| |
| panel.style.position = 'fixed';
| |
| panel.style.left = '20px';
| |
| panel.style.bottom = '20px';
| |
| panel.style.padding = '12px 16px';
| |
| panel.style.fontSize = '13px';
| |
| panel.style.background = '#fff';
| |
| panel.style.border = '1px solid #ccc';
| |
| panel.style.borderRadius = '8px';
| |
| panel.style.boxShadow = '0 2px 10px rgba(0,0,0,0.1)';
| |
| panel.style.zIndex = '2000';
| |
| panel.style.minWidth = '220px';
| |
|
| |
| /* Close Button (X) */
| |
| var closeBtn = document.createElement('div');
| |
| closeBtn.textContent = '✕';
| |
| closeBtn.style.position = 'absolute';
| |
| closeBtn.style.top = '4px';
| |
| closeBtn.style.right = '8px';
| |
| closeBtn.style.cursor = 'pointer';
| |
| closeBtn.style.fontSize = '14px';
| |
| closeBtn.style.color = '#666';
| |
| closeBtn.onclick = function () {
| |
| panel.style.display = 'none';
| |
| document.getElementById('wt-open-btn').style.display = 'block';
| |
| };
| |
| panel.appendChild(closeBtn);
| |
|
| |
| /* Title */
| |
| var title = document.createElement('div');
| |
| title.textContent = 'Writer Tools';
| |
| title.style.fontWeight = 'bold';
| |
| title.style.marginBottom = '6px';
| |
| panel.appendChild(title);
| |
|
| |
| /* Stats */
| |
| var wordsDiv = document.createElement('div');
| |
| wordsDiv.id = 'wt-words';
| |
| wordsDiv.textContent = 'Words: 0';
| |
| panel.appendChild(wordsDiv);
| |
|
| |
| var charsDiv = document.createElement('div');
| |
| charsDiv.id = 'wt-chars';
| |
| charsDiv.textContent = 'Characters: 0';
| |
| panel.appendChild(charsDiv);
| |
|
| |
| var rtDiv = document.createElement('div');
| |
| rtDiv.id = 'wt-rt';
| |
| rtDiv.textContent = 'Reading time: 0 min';
| |
| panel.appendChild(rtDiv);
| |
|
| |
| document.body.appendChild(panel);
| |
|
| |
| /* OPEN Button (Floating) */
| |
| var openBtn = document.createElement('div');
| |
| openBtn.id = 'wt-open-btn';
| |
| openBtn.textContent = 'Writer Tools';
| |
| openBtn.style.position = 'fixed';
| |
| openBtn.style.left = '20px';
| |
| openBtn.style.bottom = '20px';
| |
| openBtn.style.padding = '6px 10px';
| |
| openBtn.style.background = '#0366d6';
| |
| openBtn.style.color = '#fff';
| |
| openBtn.style.borderRadius = '6px';
| |
| openBtn.style.cursor = 'pointer';
| |
| openBtn.style.fontSize = '12px';
| |
| openBtn.style.zIndex = '2001';
| |
| openBtn.style.display = 'none';
| |
| openBtn.onclick = function () {
| |
| panel.style.display = 'block';
| |
| openBtn.style.display = 'none';
| |
| };
| |
|
| |
| document.body.appendChild(openBtn);
| |
| }
| |
|
| |
| /* --------------------------------------------------------------
| |
| Text Utilities
| |
| -------------------------------------------------------------- */
| |
| function cleanText(str) {
| |
| if (!str) return '';
| |
| str = str.replace(/\u00A0/g, ' '); // non-breaking spaces
| |
| return str.replace(/\s+/g, ' ').trim();
| |
| }
| |
|
| |
| function countWords(str) {
| |
| if (!str) return 0;
| |
| var arr = str.split(/\s+/);
| |
| var c = 0;
| |
| for (var i = 0; i < arr.length; i++) {
| |
| if (/[A-Za-z0-9]/.test(arr[i])) c++;
| |
| }
| |
| return c;
| |
| }
| |
|
| |
| function countChars(str) {
| |
| return str ? str.length : 0;
| |
| }
| |
|
| |
| /* --------------------------------------------------------------
| |
| Update Panel Stats
| |
| -------------------------------------------------------------- */
| |
| function updateStats() {
| |
| if (!surfaceRef) return;
| |
|
| |
| try {
| |
| var view = surfaceRef.getView();
| |
| if (!view) return;
| |
|
| |
| var text = view.$element.text() || '';
| |
| text = cleanText(text);
| |
|
| |
| var words = countWords(text);
| |
| var chars = countChars(text);
| |
| var minutes = Math.ceil(words / WPM);
| |
| if (minutes < 1) minutes = 1;
| |
|
| |
| document.getElementById('wt-words').textContent = 'Words: ' + words;
| |
| document.getElementById('wt-chars').textContent = 'Characters: ' + chars;
| |
| document.getElementById('wt-rt').textContent = 'Reading time: ' + minutes + ' min';
| |
| } catch (err) {
| |
| console.error('WriterTools update error:', err);
| |
| }
| |
| }
| |
|
| |
| /* --------------------------------------------------------------
| |
| Attach to VisualEditor surface
| |
| -------------------------------------------------------------- */
| |
| function attachToVE() {
| |
| try {
| |
| surfaceRef = null;
| |
|
| |
| if (
| |
| ve &&
| |
| ve.init &&
| |
| ve.init.target &&
| |
| ve.init.target.getSurface
| |
| ) {
| |
| surfaceRef = ve.init.target.getSurface();
| |
| }
| |
|
| |
| if (!surfaceRef) return;
| |
|
| |
| createPanel();
| |
| updateStats();
| |
|
| |
| /* Update while typing (throttle 200ms) */
| |
| surfaceRef.getModel().on('transact', function () {
| |
| if (updateTimeout) clearTimeout(updateTimeout);
| |
| updateTimeout = setTimeout(updateStats, 200);
| |
| });
| |
|
| |
| } catch (e) {
| |
| console.error('WriterTools attach error:', e);
| |
| }
| |
| }
| |
|
| |
| /* --------------------------------------------------------------
| |
| Trigger when VE activates
| |
| -------------------------------------------------------------- */
| |
| mw.hook('ve.activationComplete').add(function () {
| |
| setTimeout(attachToVE, 400);
| |
| });
| |
|
| |
| })();
| |
|
| |
|
| |
|
| |
|
| |
|
| |
| // Team Dashboard Settings
| |
|
| |
| mw.loader.using(['mediawiki.api', 'mediawiki.util']).then(function () {
| |
| (function ($) {
| |
|
| |
| /** ---------- Utility: escape HTML ---------- **/
| |
| function escapeHtml(str) {
| |
| return String(str)
| |
| .replace(/&/g, '&')
| |
| .replace(/</g, '<')
| |
| .replace(/>/g, '>')
| |
| .replace(/"/g, '"')
| |
| .replace(/'/g, ''');
| |
| }
| |
|
| |
| /** ---------- Detect Allowed Special Pages ---------- **/
| |
| var pageName = (mw.config.get('wgCanonicalSpecialPageName') || '').toLowerCase();
| |
| var allowedPages = ['teamdashboard', 'approvedpages'];
| |
| if (!allowedPages.includes(pageName)) return;
| |
|
| |
|
| |
| /** ---------- Accordion UI (with LocalStorage) ---------- **/
| |
| function initAccordionOnce() {
| |
| $('.accordion-toggle').each(function () {
| |
| var $el = $(this);
| |
| var targetSel = $el.attr('data-target');
| |
| var $target = $(targetSel);
| |
| if (!$target.length) return;
| |
|
| |
| if ($el.find('.pa-arrow').length === 0) {
| |
| $el.prepend('<span class="pa-arrow">▶</span> ');
| |
| }
| |
|
| |
| var saved = localStorage.getItem('pa:' + targetSel);
| |
|
| |
| if (saved === 'open') {
| |
| $target.show();
| |
| $el.addClass('open');
| |
| $el.find('.pa-arrow').text('▼ ');
| |
| } else {
| |
| $target.hide();
| |
| $el.removeClass('open');
| |
| $el.find('.pa-arrow').text('▶ ');
| |
| }
| |
|
| |
| if (!$el.data('pa-bound')) {
| |
| $el.on('click', function () {
| |
| var isOpen = $el.hasClass('open');
| |
|
| |
| if (isOpen) {
| |
| $el.removeClass('open');
| |
| $target.slideUp(150);
| |
| $el.find('.pa-arrow').text('▶ ');
| |
| localStorage.setItem('pa:' + targetSel, 'closed');
| |
| } else {
| |
| $el.addClass('open');
| |
| $target.slideDown(150);
| |
| $el.find('.pa-arrow').text('▼ ');
| |
| localStorage.setItem('pa:' + targetSel, 'open');
| |
| }
| |
| });
| |
|
| |
| $el.data('pa-bound', true);
| |
| }
| |
| });
| |
| }
| |
|
| |
|
| |
| /** ---------- MediaWiki API ---------- **/
| |
| var api = new mw.Api();
| |
| function getFreshToken() {
| |
| return api
| |
| .get({
| |
| action: 'query',
| |
| meta: 'tokens',
| |
| type: 'csrf',
| |
| format: 'json'
| |
| })
| |
| .then(function (d) {
| |
| return d.query.tokens.csrftoken || '';
| |
| })
| |
| .catch(function () {
| |
| return '';
| |
| });
| |
| }
| |
|
| |
| /** ---------- POST helper ---------- **/
| |
| function postAction(fd) {
| |
| return $.ajax({
| |
| url: mw.util.getUrl('Special:TeamDashboard'),
| |
| type: 'POST',
| |
| data: fd,
| |
| processData: false,
| |
| contentType: false,
| |
| headers: { 'X-Requested-With': 'XMLHttpRequest' }
| |
| });
| |
| }
| |
|
| |
| /** ---------- Approve / Reject ---------- **/
| |
| $(document).on('click', 'button[data-action]', function (e) {
| |
| e.preventDefault();
| |
| var $btn = $(this);
| |
| if ($btn.prop('disabled')) return;
| |
|
| |
| var action = $btn.data('action'),
| |
| pageId = $btn.data('page-id');
| |
|
| |
| $btn.prop('disabled', true);
| |
|
| |
| getFreshToken().then(function (token) {
| |
| if (!token) {
| |
| alert('Session expired, reload.');
| |
| $btn.prop('disabled', false);
| |
| return;
| |
| }
| |
|
| |
| var fd = new FormData();
| |
| fd.append('pa_action', action);
| |
| fd.append('page_id', pageId);
| |
| fd.append('token', token);
| |
|
| |
| postAction(fd)
| |
| .done(function () {
| |
| if (action === 'approve') {
| |
| $btn.text('Approved ✓').prop('disabled', true);
| |
| $btn.closest('td').prev().prev().text('approved');
| |
| } else if (action === 'reject') {
| |
| $btn.text('Rejected ✗').prop('disabled', true);
| |
| $btn.closest('td').prev().prev().text('rejected');
| |
| }
| |
| })
| |
| .fail(function () {
| |
| alert('Action failed. Please try again.');
| |
| $btn.prop('disabled', false);
| |
| });
| |
| });
| |
| });
| |
|
| |
| /** ---------- Comment submit ---------- **/
| |
| $(document).on('click', '.comment-submit', function (e) {
| |
| e.preventDefault();
| |
| var $btn = $(this);
| |
| if ($btn.prop('disabled')) return;
| |
|
| |
| var $wrap = $btn.closest('.ajax-comment-wrap'),
| |
| txt = $.trim($wrap.find('input.comment-input').val() || ''),
| |
| pageId = $btn.data('page-id');
| |
|
| |
| if (txt === '') return;
| |
| $btn.prop('disabled', true);
| |
|
| |
| getFreshToken().then(function (token) {
| |
| if (!token) {
| |
| alert('Session expired, reload.');
| |
| $btn.prop('disabled', false);
| |
| return;
| |
| }
| |
|
| |
| var fd = new FormData();
| |
| fd.append('pa_action', 'comment');
| |
| fd.append('page_id', pageId);
| |
| fd.append('token', token);
| |
| fd.append('comment', txt);
| |
|
| |
| postAction(fd)
| |
| .done(function () {
| |
| var $td = $btn.closest('td');
| |
| var $list = $td.find('.comment-list');
| |
| if (!$list.length) {
| |
| var box = $('<div class="comment-box"><b>Comments</b><ul class="comment-list"></ul></div>');
| |
| $td.append(box);
| |
| $list = box.find('.comment-list');
| |
| }
| |
|
| |
| var username = mw.config.get('wgUserName') || 'You';
| |
| var now = new Date();
| |
| var istOffset = 5.5 * 60 * 60 * 1000;
| |
| var ist = new Date(now.getTime() + istOffset);
| |
|
| |
| var stamp =
| |
| ist.getFullYear() +
| |
| '-' +
| |
| String(ist.getMonth() + 1).padStart(2, '0') +
| |
| '-' +
| |
| String(ist.getDate()).padStart(2, '0') +
| |
| ' ' +
| |
| String(ist.getHours()).padStart(2, '0') +
| |
| ':' +
| |
| String(ist.getMinutes()).padStart(2, '0') +
| |
| ':' +
| |
| String(ist.getSeconds()).padStart(2, '0') +
| |
| ' IST';
| |
|
| |
| var li = $('<li/>').html(
| |
| '<b>' +
| |
| escapeHtml(username) +
| |
| '</b> <span class="date">(' +
| |
| stamp +
| |
| ')</span><br>' +
| |
| escapeHtml(txt) +
| |
| ' <button class="edit-comment-btn">✏️</button>'
| |
| );
| |
|
| |
| $list.prepend(li);
| |
| $wrap.find('input.comment-input').val('');
| |
| })
| |
| .always(function () {
| |
| $btn.prop('disabled', false);
| |
| });
| |
| });
| |
| });
| |
|
| |
| /** ---------- Delete comment ---------- **/
| |
| $(document).on('click', '.delete-comment-btn', function (e) {
| |
| e.preventDefault();
| |
| var $btn = $(this);
| |
| if ($btn.prop('disabled')) return;
| |
| if (!confirm('Delete this comment?')) return;
| |
|
| |
| var commentId = $btn.data('comment-id'),
| |
| pageId = $btn.data('page-id');
| |
|
| |
| $btn.prop('disabled', true);
| |
|
| |
| getFreshToken().then(function (token) {
| |
| if (!token) {
| |
| alert('Session expired.');
| |
| $btn.prop('disabled', false);
| |
| return;
| |
| }
| |
|
| |
| var fd = new FormData();
| |
| fd.append('pa_action', 'delete_comment');
| |
| fd.append('comment_id', commentId);
| |
| fd.append('page_id', pageId);
| |
| fd.append('token', token);
| |
|
| |
| postAction(fd)
| |
| .done(function () {
| |
| $btn.closest('li').fadeOut(200, function () {
| |
| $(this).remove();
| |
| });
| |
| })
| |
| .always(function () {
| |
| $btn.prop('disabled', false);
| |
| });
| |
| });
| |
| });
| |
|
| |
| /** ---------- FIXED: Edit comment handler ---------- **/
| |
| $(document).on('click', '.edit-comment-btn', function () {
| |
| var $btn = $(this);
| |
| var $li = $btn.closest('li');
| |
|
| |
| var oldText = $li.clone().children().remove().end().text().trim();
| |
| var pageId = $li.closest('td').find('.comment-submit').data('page-id');
| |
| var commentId = $btn.data('comment-id');
| |
|
| |
| if ($li.find('.edit-comment-area').length) return;
| |
|
| |
| var $area = $('<textarea class="edit-comment-area"></textarea>').val(oldText);
| |
| var $save = $('<button class="pa-btn save-edit">Save</button>');
| |
| var $cancel = $('<button class="pa-btn danger cancel-edit">Cancel</button>');
| |
| $li.append($area, $save, $cancel);
| |
|
| |
| $save.on('click', function () {
| |
| var newText = $.trim($area.val());
| |
| if (!newText) return alert('Comment cannot be empty.');
| |
|
| |
| getFreshToken().then(function (token) {
| |
| var fd = new FormData();
| |
| fd.append('pa_action', 'edit_comment');
| |
| fd.append('comment_id', commentId);
| |
| fd.append('page_id', pageId);
| |
| fd.append('comment', newText);
| |
| fd.append('token', token);
| |
|
| |
| postAction(fd)
| |
| .done(function () {
| |
| alert('Comment updated successfully.');
| |
|
| |
| $area.remove();
| |
| $save.remove();
| |
| $cancel.remove();
| |
|
| |
| $btn.replaceWith(
| |
| '<span>' +
| |
| escapeHtml(newText) +
| |
| ' <em>(edited)</em></span>'
| |
| );
| |
| })
| |
| .fail(function () {
| |
| alert('Comment updated successfully.');
| |
| });
| |
| });
| |
| });
| |
|
| |
| $cancel.on('click', function () {
| |
| $area.remove();
| |
| $save.remove();
| |
| $cancel.remove();
| |
| });
| |
| });
| |
|
| |
| /** ---------- Search Filter (Dashboard + ApprovedPages) ---------- **/
| |
| $(document).on('input', '.pa-search', function () {
| |
| var q = $(this).val().toLowerCase();
| |
|
| |
| $('.approved-table tbody tr, .dashboard-table tbody tr').each(function () {
| |
| $(this).toggle($(this).text().toLowerCase().indexOf(q) !== -1);
| |
| });
| |
| });
| |
| /** ---------- EXTRA SEARCH FOR ACCORDION (Leads + Writers) ---------- **/
| |
| $(document).on('input', '.pa-search', function () {
| |
| var q = $(this).val().toLowerCase();
| |
|
| |
| // 1. Filter approved + pending tables (existing behavior)
| |
| $('.approved-table tbody tr, .dashboard-table tbody tr').each(function () {
| |
| $(this).toggle($(this).text().toLowerCase().indexOf(q) !== -1);
| |
| });
| |
|
| |
| // 2. Filter accordion lead items
| |
| $('.accordion-item').each(function () {
| |
| var txt = $(this).text().toLowerCase();
| |
| if (txt.indexOf(q) !== -1) {
| |
| $(this).show();
| |
| } else {
| |
| $(this).hide();
| |
| }
| |
| });
| |
| });
| |
|
| |
| /** ---------- Sort Approved Pages ---------- **/
| |
| $(document).on('change', '.pa-sort', function () {
| |
| var mode = $(this).val();
| |
| var rows = $('.approved-table tbody tr').get();
| |
|
| |
| rows.sort(function (a, b) {
| |
| if (mode === 'az') {
| |
| var ta = $(a).find('.title').text().toLowerCase();
| |
| var tb = $(b).find('.title').text().toLowerCase();
| |
| return ta.localeCompare(tb);
| |
| }
| |
|
| |
| if (mode === 'updated') {
| |
| var da = new Date($(a).data('updated'));
| |
| var db = new Date($(b).data('updated'));
| |
| return db - da; // newest first
| |
| }
| |
| return 0;
| |
| });
| |
|
| |
| $.each(rows, function (_, row) {
| |
| $('.approved-table tbody').append(row);
| |
| });
| |
| });
| |
|
| |
| /** ---------- Init ---------- **/
| |
| initAccordionOnce();
| |
| setTimeout(initAccordionOnce, 700);
| |
|
| |
| })(jQuery);
| |
| });
| |
|
| |
| /** ---------- Article Header Sort (Pending Pages) ---------- **/
| |
| $(document).on('change', '.pa-article-sort', function () {
| |
| var mode = $(this).val();
| |
|
| |
| var $table = $(this).closest('table');
| |
| var $tbody = $table.find('tbody');
| |
| if (!$tbody.length) return;
| |
|
| |
| var rows = $tbody.find('tr').get();
| |
|
| |
| rows.sort(function (a, b) {
| |
| var ta = $(a).data('title');
| |
| var tb = $(b).data('title');
| |
|
| |
| var da = new Date($(a).data('time'));
| |
| var db = new Date($(b).data('time'));
| |
|
| |
| if (mode === 'az') return ta.localeCompare(tb);
| |
| if (mode === 'za') return tb.localeCompare(ta);
| |
| if (mode === 'oldest') return da - db;
| |
| if (mode === 'newest') return db - da;
| |
|
| |
| return 0;
| |
| });
| |
|
| |
| $.each(rows, function (_, row) {
| |
| $tbody.append(row);
| |
| });
| |
| });
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
| (function () {
| |
|
| |
| // ===================================================================
| |
| // A) FLOATING CREATE BUTTON UI (unchanged, ES5)
| |
| // ===================================================================
| |
| function initCreateButton() {
| |
| if (typeof mw === 'undefined') return;
| |
| if (!mw.config.get('wgUserName')) return;
| |
|
| |
| var groups = mw.config.get('wgUserGroups') || [];
| |
| var isEditor = groups.indexOf('editor') !== -1;
| |
| var isAdmin = groups.indexOf('sysop') !== -1;
| |
| var isWriter = groups.indexOf('writer') !== -1;
| |
| var isLead = groups.indexOf('lead') !== -1;
| |
|
| |
| // Only Writers, Leads, Editors, Admins can use UI
| |
| if (!(isWriter || isLead || isEditor || isAdmin)) return;
| |
|
| |
| // Avoid duplicate button
| |
| if (document.getElementById('floatingCreateBtn')) return;
| |
|
| |
| // Floating button
| |
| var btn = document.createElement('button');
| |
| btn.id = 'floatingCreateBtn';
| |
| btn.appendChild(document.createTextNode('+'));
| |
|
| |
| btn.style.cssText =
| |
| 'position:fixed;bottom:30px;right:30px;width:60px;height:60px;' +
| |
| 'border-radius:50%;background:#0078D7;color:#fff;font-size:32px;' +
| |
| 'border:none;cursor:pointer;z-index:9999;box-shadow:0 4px 10px rgba(0,0,0,0.3);';
| |
|
| |
| btn.onmouseover = function () { btn.style.background = '#005fa3'; };
| |
| btn.onmouseout = function () { btn.style.background = '#0078D7'; };
| |
|
| |
| document.body.appendChild(btn);
| |
|
| |
| // Popup box
| |
| var popup = document.createElement('div');
| |
| popup.id = 'createPopupBox';
| |
| popup.style.cssText =
| |
| 'position:fixed;bottom:100px;right:30px;background:#fff;padding:15px;' +
| |
| 'border:2px solid #ccc;border-radius:12px;width:300px;display:none;' +
| |
| 'box-shadow:0 6px 14px rgba(0,0,0,0.25);z-index:10000;';
| |
|
| |
| var html = '';
| |
|
| |
| // Language selector
| |
| html += '<label style="font-weight:bold;">Select Language</label><br>';
| |
| html += '<select id="pageLang" style="width:100%;padding:6px;margin:6px 0;border:1px solid #ccc;border-radius:5px;">';
| |
| html += '<option value="en">English</option>';
| |
| html += '<option value="hi">Hindi</option>';
| |
| html += '<option value="ta">Tamil</option>';
| |
| html += '</select>';
| |
|
| |
| // Page creation
| |
| html += '<label style="font-weight:bold;">Create new page</label>';
| |
| html += '<input type="text" id="newPageName" placeholder="Enter page name"' +
| |
| 'style="width:100%;padding:6px;margin-top:4px;border:1px solid #ccc;border-radius:5px;">';
| |
|
| |
| html += '<button id="createPageBtn" style="margin-top:8px;width:100%;background:#0078D7;' +
| |
| 'color:white;border:none;padding:6px;border-radius:6px;cursor:pointer;">Create Page</button>';
| |
|
| |
| // ---------- CATEGORY CREATION (Editor/Admin only) ----------
| |
| if (isEditor || isAdmin) {
| |
| html += '<hr style="margin:10px 0;">';
| |
| html += '<label style="font-weight:bold;">Create new category</label>';
| |
| html += '<input type="text" id="newCategoryName" placeholder="Enter category name"' +
| |
| 'style="width:100%;padding:6px;margin-top:4px;border:1px solid #ccc;border-radius:5px;">';
| |
|
| |
| html += '<button id="createCategoryBtn" style="margin-top:8px;width:100%;background:#28a745;' +
| |
| 'color:white;border:none;padding:6px;border-radius:6px;cursor:pointer;">Create Category</button>';
| |
| }
| |
|
| |
| // Dashboard + Mapping
| |
| html += '<hr style="margin:10px 0;">';
| |
| html += '<button id="openDashboardBtn" style="width:100%;background:#6f42c1;color:white;' +
| |
| 'border:none;padding:6px;border-radius:6px;cursor:pointer;">Dashboard</button>';
| |
|
| |
| html += '<button id="openMappingBtn" style="width:100%;background:#ff5733;color:white;' +
| |
| 'border:none;padding:6px;border-radius:6px;margin-top:6px;cursor:pointer;">Mapping</button>';
| |
|
| |
| popup.innerHTML = html;
| |
| document.body.appendChild(popup);
| |
|
| |
| // Toggle popup
| |
| btn.onclick = function () {
| |
| popup.style.display = (popup.style.display === 'none') ? 'block' : 'none';
| |
| };
| |
|
| |
| // Popup click handler
| |
| popup.onclick = function (ev) {
| |
| var target = ev.target;
| |
|
| |
| // Create normal page
| |
| if (target.id === 'createPageBtn') {
| |
| var lang = document.getElementById('pageLang').value;
| |
| var name = document.getElementById('newPageName').value.trim();
| |
|
| |
| if (!name) return alert("Please enter a page name");
| |
|
| |
| var t = name.replace(/\s+/g, '_');
| |
| var url = (lang === 'hi') ? '/Hi/' + t :
| |
| (lang === 'ta') ? '/Ta/' + t :
| |
| '/' + t;
| |
|
| |
| // No categories needed here; creating new page directly
| |
| window.location.href = url + '?action=edit&veaction=edit';
| |
| }
| |
|
| |
| // Create category
| |
| if (target.id === 'createCategoryBtn') {
| |
| var cat = document.getElementById('newCategoryName').value.trim();
| |
| if (!cat) return alert("Please enter a category name");
| |
|
| |
| var cTitle = 'Category:' + cat.replace(/\s+/g, '-');
| |
| window.location.href = '/' + cTitle + '?action=edit&veaction=edit';
| |
| }
| |
|
| |
| if (target.id === 'openDashboardBtn')
| |
| window.location.href = '/Special:TeamDashboard';
| |
|
| |
| if (target.id === 'openMappingBtn')
| |
| window.location.href = '/Special:TeamMapping';
| |
| };
| |
| }
| |
|
| |
| if (document.readyState === 'complete' || document.readyState === 'interactive')
| |
| initCreateButton();
| |
| else
| |
| document.addEventListener('DOMContentLoaded', initCreateButton);
| |
|
| |
|
| |
| // ===================================================================
| |
| // B) CATEGORY + PARENT PAGE LOGIC (Original + Multilingual Enhancements)
| |
| // including category inheritance helpers
| |
| // ===================================================================
| |
| mw.loader.using(['mediawiki.util', 'mediawiki.api']).then(function () {
| |
|
| |
| var api = new mw.Api();
| |
| var user = mw.config.get('wgUserName');
| |
| if (!user) return;
| |
|
| |
| var ns = mw.config.get('wgNamespaceNumber');
| |
| var title = mw.config.get('wgTitle') || '';
| |
| var pagenm = mw.config.get('wgPageName') || '';
| |
| var action = mw.config.get('wgAction') || '';
| |
| var revId = mw.config.get('wgCurRevisionId') || 0;
| |
|
| |
| var urlParams = new URLSearchParams(location.search);
| |
| var autoCat = urlParams.get('autocategory');
| |
| var skipParent = urlParams.get('skipparent');
| |
|
| |
| // ---------------------------------------------------------------
| |
| // CATEGORY PROMPT (UNCHANGED)
| |
| // ---------------------------------------------------------------
| |
| if (ns === 14) { // Category namespace
| |
| var key = "category_prompt_done_" + title;
| |
|
| |
| function askCategory() {
| |
| if (sessionStorage.getItem(key)) return;
| |
| sessionStorage.setItem(key, 'done');
| |
|
| |
| var ask = confirm(
| |
| 'You created category "' + title + '".\n\nCreate a page with this name?'
| |
| );
| |
| if (ask) {
| |
| window.location.href = mw.util.getUrl(title, {
| |
| action:'edit', veaction:'edit',
| |
| autocategory:title,
| |
| skipparent:1
| |
| });
| |
| }
| |
| }
| |
|
| |
| mw.hook('postEdit').add(askCategory);
| |
|
| |
| $(window).on('popstate', function () {
| |
| if (action !== 'view') return;
| |
| if (sessionStorage.getItem(key)) return;
| |
|
| |
| api.get({
| |
| action:'query',
| |
| titles:'Category:' + title,
| |
| prop:'info'
| |
| }).done(function (res) {
| |
| var page = res.query.pages[Object.keys(res.query.pages)[0]];
| |
| if (page && !page.missing) askCategory();
| |
| });
| |
| });
| |
| }
| |
|
| |
| // ---------------------------------------------------------------
| |
| // INJECT AUTOCATEGORY
| |
| // ---------------------------------------------------------------
| |
| function injectCategory(cat) {
| |
| var box = $('#wpTextbox1');
| |
| if (!box.length) return;
| |
|
| |
| var tag = '[[Category:' + cat + ']]';
| |
| if (box.val().indexOf(tag) === -1)
| |
| box.val(tag + '\n\n' + box.val());
| |
| }
| |
|
| |
| if (autoCat) {
| |
| $(function () { injectCategory(autoCat); });
| |
| mw.hook('ve.activationComplete').add(function () { injectCategory(autoCat); });
| |
| }
| |
|
| |
| // ---------------------------------------------------------------
| |
| // LANGUAGE DETECTION
| |
| // ---------------------------------------------------------------
| |
| var isHindi = pagenm.indexOf("Hi/") === 0;
| |
| var isTamil = pagenm.indexOf("Ta/") === 0;
| |
| var langPrefix = isHindi ? "Hi/" : (isTamil ? "Ta/" : "");
| |
|
| |
| // Remove prefix from title
| |
| function stripPrefix(t) {
| |
| return t.replace(/^Hi\//, "").replace(/^Ta\//, "");
| |
| }
| |
|
| |
| // Build clean title
| |
| function buildTitle(parent, child) {
| |
| parent = stripPrefix(parent).replace(/^\/+|\/+$/g, "");
| |
| child = stripPrefix(child ).replace(/^\/+|\/+$/g, "");
| |
|
| |
| if (!parent)
| |
| return langPrefix + child;
| |
|
| |
| return langPrefix + parent + "/" + child;
| |
| }
| |
|
| |
| // ---------------------------------------------------------------
| |
| // HELPER: Get parent categories (exposed globally)
| |
| // ---------------------------------------------------------------
| |
| window.getParentCategories = function (parentTitle, callback) {
| |
| if (!parentTitle) {
| |
| callback([]);
| |
| return;
| |
| }
| |
| api.get({
| |
| action: "query",
| |
| prop: "categories",
| |
| titles: parentTitle,
| |
| cllimit: "max"
| |
| }).done(function (res) {
| |
| try {
| |
| var page = res.query.pages[Object.keys(res.query.pages)[0]];
| |
| var cats = [];
| |
| if (page && page.categories) {
| |
| for (var i = 0; i < page.categories.length; i++) {
| |
| var c = page.categories[i].title.replace(/^Category:/, '');
| |
| cats.push(c);
| |
| }
| |
| }
| |
| callback(cats);
| |
| } catch (ee) {
| |
| callback([]);
| |
| }
| |
| }).fail(function () {
| |
| callback([]);
| |
| });
| |
| };
| |
|
| |
| // ---------------------------------------------------------------
| |
| // HELPER: Insert categories into editor (VE or Source)
| |
| // Runs once per stored inheritCategories session value
| |
| // ---------------------------------------------------------------
| |
| function applyInheritedCategories() {
| |
| var data = sessionStorage.getItem("inheritCategories");
| |
| if (!data) return;
| |
| try {
| |
| var cats = JSON.parse(data);
| |
| } catch (e) {
| |
| sessionStorage.removeItem("inheritCategories");
| |
| return;
| |
| }
| |
| if (!cats || !cats.length) {
| |
| sessionStorage.removeItem("inheritCategories");
| |
| return;
| |
| }
| |
|
| |
| // Remove to avoid double-insert
| |
| sessionStorage.removeItem("inheritCategories");
| |
|
| |
| // Build wikitext fragment
| |
| var wikitext = "";
| |
| for (var i = 0; i < cats.length; i++) {
| |
| wikitext += "[[Category:" + cats[i] + "]]\n";
| |
| }
| |
|
| |
| // 1) If source editor present, insert there
| |
| var box = document.getElementById("wpTextbox1");
| |
| if (box) {
| |
| box.value = wikitext + "\n" + box.value;
| |
| return;
| |
| }
| |
|
| |
| // 2) VisualEditor - try to insert using VE model
| |
| try {
| |
| if (window.ve && ve.init && ve.init.target) {
| |
| var surface = ve.init.target.getSurface();
| |
| if (!surface || !surface.getModel) {
| |
| // VE not ready yet: retry shortly
| |
| setTimeout(applyInheritedCategories, 200);
| |
| return;
| |
| }
| |
| var model = surface.getModel();
| |
| // Attempt insertion; wrap in try/catch to avoid breaking if APIs differ
| |
| try {
| |
| model.getDocument().transact(model.insertContent(wikitext + "\n"));
| |
| } catch (e) {
| |
| // As a fallback, try to insert by collapsing selection and inserting text
| |
| try {
| |
| var frag = model.getDocument().getFragment();
| |
| if (frag && frag.collapseToEnd) {
| |
| frag.collapseToEnd();
| |
| frag.insertContent("\n" + wikitext);
| |
| }
| |
| } catch (e2) {
| |
| // give up silently - categories already removed from session
| |
| }
| |
| }
| |
| }
| |
| } catch (ee) {
| |
| // nothing
| |
| }
| |
| }
| |
|
| |
| // Run at DOM ready (source editor case)
| |
| $(applyInheritedCategories);
| |
| // Run after VE activation (VE case)
| |
| mw.hook('ve.activationComplete').add(applyInheritedCategories);
| |
|
| |
|
| |
| // ---------------------------------------------------------------
| |
| // PARENT PAGE MODAL (made global so VE can call it: window.askParent)
| |
| // + Cancel and click-outside behavior
| |
| // ---------------------------------------------------------------
| |
| window.askParent = function (child, done) {
| |
| if (window.__parentModalShown) return;
| |
| window.__parentModalShown = true;
| |
|
| |
| var o = $('<div>').css({
| |
| position:'fixed',top:0,left:0,width:'100%',height:'100%',
| |
| background:'rgba(0,0,0,0.5)',display:'flex',
| |
| justifyContent:'center',alignItems:'center',
| |
| zIndex:999999
| |
| });
| |
|
| |
| var box = $('<div>').css({
| |
| background:'#fff',padding:'20px',width:'420px',
| |
| borderRadius:'10px',boxShadow:'0 4px 12px rgba(0,0,0,0.3)',
| |
| position:'relative'
| |
| });
| |
|
| |
| var input = $('<input type="text" placeholder="Search parent...">').css({
| |
| width:'100%',padding:'8px',border:'1px solid #ccc',borderRadius:'6px'
| |
| });
| |
|
| |
| var list = $('<ul>').css({
| |
| listStyle:'none',padding:0,margin:'10px 0 0',
| |
| maxHeight:'200px',overflowY:'auto',border:'1px solid #ddd',
| |
| borderRadius:'6px',display:'none'
| |
| });
| |
|
| |
| var btns = $('<div>').css({ marginTop:'10px', textAlign:'right' });
| |
|
| |
| var ok = $('<button>Confirm</button>').css({
| |
| background:'#007bff',color:'#fff',padding:'8px 14px',
| |
| border:'none',borderRadius:'6px',cursor:'pointer'
| |
| });
| |
|
| |
| var skip = $('<button>No Parent</button>').css({
| |
| background:'#6c757d',color:'#fff',padding:'8px 14px',
| |
| border:'none',borderRadius:'6px',cursor:'pointer',
| |
| marginLeft:'8px'
| |
| });
| |
|
| |
| var cancel = $('<button>Cancel</button>').css({
| |
| background:'#dc3545',color:'#fff',padding:'8px 14px',
| |
| border:'none',borderRadius:'6px',cursor:'pointer',
| |
| marginLeft:'8px'
| |
| });
| |
|
| |
| btns.append(ok, skip, cancel);
| |
| box.append('<h3>Select Parent Page</h3>', input, list, btns);
| |
| o.append(box);
| |
| $('body').append(o);
| |
|
| |
| var selected = '';
| |
|
| |
| input.on('input', function () {
| |
| var q = $.trim(input.val());
| |
| list.empty();
| |
| if (!q) return list.hide();
| |
|
| |
| api.get({
| |
| action:'opensearch',
| |
| search: langPrefix + q,
| |
| limit: 12,
| |
| namespace:0
| |
| }).done(function (res) {
| |
| var results = res[1] || [];
| |
| list.empty();
| |
| if (!results.length) return list.hide();
| |
| list.show();
| |
|
| |
| for (var i = 0; i < results.length; i++) {
| |
| (function(pg){
| |
| var li = $('<li>').text(pg).css({
| |
| padding:'8px',cursor:'pointer'
| |
| }).hover(
| |
| function(){ $(this).css('background','#eee'); },
| |
| function(){ $(this).css('background',''); }
| |
| ).click(function(){
| |
| selected = pg;
| |
| input.val(stripPrefix(pg));
| |
| list.hide();
| |
| });
| |
| list.append(li);
| |
| })(results[i]);
| |
| }
| |
| });
| |
| });
| |
|
| |
| // Confirm
| |
| ok.on('click', function () {
| |
| var parent = selected || input.val().trim();
| |
| if (!parent) return alert("Select parent or click No Parent");
| |
| o.remove();
| |
| window.__parentModalShown = false;
| |
| done(parent);
| |
| });
| |
|
| |
| // No Parent (explicitly remove parent)
| |
| skip.on('click', function () {
| |
| o.remove();
| |
| window.__parentModalShown = false;
| |
| done("");
| |
| });
| |
|
| |
| // Cancel (do nothing)
| |
| cancel.on('click', function () {
| |
| o.remove();
| |
| window.__parentModalShown = false;
| |
| done(null);
| |
| });
| |
|
| |
| // Click outside to close (acts like Cancel)
| |
| o.on('click', function (e) {
| |
| if (e.target === o[0]) {
| |
| o.remove();
| |
| window.__parentModalShown = false;
| |
| done(null);
| |
| }
| |
| });
| |
| };
| |
|
| |
| // ---------------------------------------------------------------
| |
| // NEW PAGE HANDLING (when creating a new page via edit)
| |
| // — now stores parent categories into sessionStorage before redirect
| |
| // ---------------------------------------------------------------
| |
| function handleNewPage() {
| |
| if (ns !== 0) return;
| |
| if (action !== 'edit') return;
| |
| if (revId !== 0) return;
| |
|
| |
| if (skipParent && autoCat) return;
| |
|
| |
| askParent(pagenm, function (parent) {
| |
| // If user cancelled modal, parent === null
| |
| if (parent === null) return;
| |
|
| |
| var finalTitle = buildTitle(parent, pagenm);
| |
|
| |
| // If parent given, fetch its categories and store for the new page
| |
| if (parent) {
| |
| window.getParentCategories(parent, function(cats) {
| |
| try {
| |
| sessionStorage.setItem("inheritCategories", JSON.stringify(cats || []));
| |
| } catch (e) {}
| |
| // Redirect to edit (VE)
| |
| var url = mw.util.getUrl(finalTitle, { veaction:'edit' });
| |
| if (location.href !== url) location.href = url;
| |
| });
| |
| } else {
| |
| // Explicit "No Parent" — clear parent, but still open editor
| |
| try { sessionStorage.removeItem("inheritCategories"); } catch (e) {}
| |
| var url2 = mw.util.getUrl(finalTitle, { veaction:'edit' });
| |
| if (location.href !== url2) location.href = url2;
| |
| }
| |
| });
| |
| }
| |
|
| |
| if (action === 'edit') handleNewPage();
| |
| mw.hook('ve.activationComplete').add(handleNewPage);
| |
|
| |
| }); // end mw.loader.using for Section B
| |
|
| |
|
| |
| // ===================================================================
| |
| // C) CHANGE PARENT PAGE (VE PAGE SETTINGS ONLY) — CLEAN FINAL VERSION
| |
| // ===================================================================
| |
| mw.loader.using(['mediawiki.util', 'mediawiki.api']).then(function () {
| |
|
| |
| if (window.__changeParentInjected) return;
| |
| window.__changeParentInjected = true;
| |
|
| |
| var ns = mw.config.get('wgNamespaceNumber');
| |
| var action = mw.config.get('wgAction') || "";
| |
| var revId = mw.config.get('wgCurRevisionId') || 0;
| |
|
| |
| // only main namespace & editing
| |
| if (ns !== 0) return;
| |
| var isVE = location.search.indexOf("veaction=edit") !== -1;
| |
| var isEdit = action === "edit";
| |
| if (!isVE && !isEdit) return;
| |
| if (!revId) return;
| |
|
| |
| var groups = mw.config.get("wgUserGroups") || [];
| |
| var allowed =
| |
| groups.indexOf("writer") !== -1 ||
| |
| groups.indexOf("lead") !== -1 ||
| |
| groups.indexOf("editor") !== -1 ||
| |
| groups.indexOf("sysop") !== -1;
| |
|
| |
| if (!allowed) return;
| |
|
| |
|
| |
| // Use a robust wait for VE toolbar + settings popup
| |
| function waitAndInject() {
| |
| var $settingsTool = $('span.oo-ui-tool-name-settings');
| |
| if (!$settingsTool.length) {
| |
| return setTimeout(waitAndInject, 200);
| |
| }
| |
|
| |
| var $toolGroup = $settingsTool.closest(".oo-ui-toolGroup-tools");
| |
| if (!$toolGroup.length) {
| |
| return setTimeout(waitAndInject, 200);
| |
| }
| |
|
| |
| if ($("#ve-change-parent-tool").length) return;
| |
|
| |
| var $tool = $('<span>')
| |
| .attr("id", "ve-change-parent-tool")
| |
| .addClass("oo-ui-widget oo-ui-widget-enabled oo-ui-tool oo-ui-tool-link")
| |
| .css({ display: "block" });
| |
|
| |
| var $link = $('<a>')
| |
| .addClass("oo-ui-tool-link")
| |
| .attr("tabindex", "0")
| |
| .css({
| |
| display: "flex",
| |
| alignItems: "center",
| |
| padding: "6px"
| |
| });
| |
|
| |
| // Use hierarchy icon to avoid overlap problems
| |
| var $icon = $('<span>')
| |
| .addClass("oo-ui-iconElement-icon oo-ui-icon-hierarchy")
| |
| .css({ marginRight: "6px" });
| |
|
| |
| var $label = $('<span>')
| |
| .addClass("oo-ui-tool-title")
| |
| .text("Change Parent");
| |
|
| |
| $link.append($icon).append($label);
| |
| $tool.append($link);
| |
| $toolGroup.append($tool);
| |
|
| |
| // OOUI-safe click
| |
| $link.on("click", function (e) {
| |
| e.preventDefault();
| |
| e.stopPropagation();
| |
|
| |
| if (typeof window.askParent !== "function") {
| |
| alert("Parent selector not ready. Reload editor.");
| |
| return;
| |
| }
| |
|
| |
| var fullTitle = mw.config.get("wgPageName") || "";
| |
|
| |
| // Detect language prefix
| |
| var prefix = "";
| |
| if (fullTitle.indexOf("Hi/") === 0) prefix = "Hi/";
| |
| if (fullTitle.indexOf("Ta/") === 0) prefix = "Ta/";
| |
|
| |
| var noPrefix = fullTitle.replace(/^Hi\//, "").replace(/^Ta\//, "");
| |
| var parts = noPrefix.split("/");
| |
| var child = parts[parts.length - 1];
| |
|
| |
| window.askParent(fullTitle, function (parent) {
| |
|
| |
| // parent === null means cancel
| |
| if (parent === null) return;
| |
|
| |
| parent = parent.replace(/^Hi\//, "")
| |
| .replace(/^Ta\//, "")
| |
| .trim();
| |
|
| |
| var newTitle = parent ?
| |
| prefix + parent + "/" + child :
| |
| prefix + child;
| |
|
| |
| newTitle = newTitle.replace(/\/+/g, "/");
| |
|
| |
| if (newTitle === fullTitle) {
| |
| alert("Parent not changed.");
| |
| return;
| |
| }
| |
|
| |
| // Before moving, fetch parent categories for the new parent path
| |
| var parentForCats = parent || "";
| |
| if (parentForCats) {
| |
| // store categories so new page gets them after move+redirect
| |
| window.getParentCategories(parentForCats, function (cats) {
| |
| try {
| |
| sessionStorage.setItem("inheritCategories", JSON.stringify(cats || []));
| |
| } catch (e) {}
| |
| // perform move
| |
| movePage(fullTitle, newTitle);
| |
| });
| |
| } else {
| |
| // No parent selected (move to top-level)
| |
| try { sessionStorage.removeItem("inheritCategories"); } catch (e) {}
| |
| movePage(fullTitle, newTitle);
| |
| }
| |
| });
| |
| });
| |
|
| |
| console.log("✔ Change Parent added to VE Page Settings");
| |
| }
| |
|
| |
| // Wait for VE activation and then try injecting (more robust)
| |
| mw.hook('ve.activationComplete').add(function () {
| |
| waitAndInject();
| |
| });
| |
| // Also attempt immediately (in case VE already activated)
| |
| setTimeout(waitAndInject, 300);
| |
|
| |
|
| |
| // ===================================================================
| |
| // MOVE PAGE (unchanged except parent-cats handling is done before calling movePage)
| |
| // ===================================================================
| |
| function movePage(oldTitle, newTitle) {
| |
|
| |
| var api = new mw.Api();
| |
|
| |
| api.postWithToken("csrf", {
| |
| action: "move",
| |
| from: oldTitle,
| |
| to: newTitle,
| |
| reason: "Updated parent structure",
| |
| movetalk: 1,
| |
| noredirect: 1
| |
| })
| |
| .done(function () {
| |
|
| |
| api.postWithToken("csrf", {
| |
| action: "delete",
| |
| title: oldTitle,
| |
| reason: "Old parent path cleaned after move"
| |
| })
| |
| .done(function () {
| |
| alert("Parent updated and old page deleted.");
| |
| // After move & delete we redirect to new title.
| |
| // applyInheritedCategories() will run on load because we pre-saved inheritCategories.
| |
| window.location.href = mw.util.getUrl(newTitle);
| |
| })
| |
| .fail(function (err) {
| |
| alert("Page moved, but deletion failed: " +
| |
| (err && err.error && err.error.info ? err.error.info : "Unknown"));
| |
| window.location.href = mw.util.getUrl(newTitle);
| |
| });
| |
|
| |
| })
| |
| .fail(function (err) {
| |
| var msg = (err && err.error && err.error.info) ? err.error.info : "Unknown error";
| |
| alert("Move failed: " + msg);
| |
| });
| |
| }
| |
|
| |
| }); // end Section C
| |
|
| |
| })();
| |
|
| |
|
| |
|
| |
| mw.loader.using(['jquery'], function () {
| |
| $(function () {
| |
|
| |
| // Run only on Special:SpecialPages
| |
| if (mw.config.get('wgCanonicalSpecialPageName') !== 'SpecialPages') {
| |
| return;
| |
| }
| |
|
| |
| var $heading = $('#mw-specialpagesgroup-other');
| |
| if (!$heading.length) {
| |
| console.warn('Other special pages heading not found');
| |
| return;
| |
| }
| |
|
| |
| var $list = $heading.next('.mw-specialpages-list');
| |
| if (!$list.length) {
| |
| console.warn('Other special pages list not found');
| |
| return;
| |
| }
| |
|
| |
| // Correct container for Chameleon & all modern skins
| |
| var $container = $('.mw-parser-output').first();
| |
| if (!$container.length) {
| |
| console.warn('Parser output container not found');
| |
| return;
| |
| }
| |
|
| |
| // Move heading + list to top
| |
| $heading.add($list).prependTo($container);
| |
| });
| |
| });
| |