MediaWiki:Gadget-ref-klawiatura.js

Z Wikipedii, wolnej encyklopedii

Uwaga: aby zobaczyć zmiany po opublikowaniu, może zajść potrzeba wyczyszczenia pamięci podręcznej przeglądarki.

  • Firefox / Safari: Przytrzymaj Shift podczas klikania Odśwież bieżącą stronę, lub naciśnij klawisze Ctrl+F5, lub Ctrl+R (⌘-R na komputerze Mac)
  • Google Chrome: Naciśnij Ctrl-Shift-R (⌘-Shift-R na komputerze Mac)
  • Internet Explorer / Edge: Przytrzymaj Ctrl, jednocześnie klikając Odśwież, lub naciśnij klawisze Ctrl+F5
  • Opera: Naciśnij klawisze Ctrl+F5.
// @ts-check
/**
 * <nowiki> <- przeciwdziała dodawaniu do kategorii z błędami rozszerzenia cite
 * 
 * Narzędzie, które ułatwia wstawianie przypisów w formie XML (<ref>) z klawiatury
 * oraz dodaje możliwość zamiany szablonów R w trakcie pisania
 *
 * Autor: [[pl:w:User:Msz2001]]
 *
 * Na podstawie skryptu:
 *    autorstwa [[pl:wikt:User:Peter Bowman]]
 *    stąd: [[pl:wikt:Specjalna:Diff/7485775]]
 */
/**
 * @typedef {{ctrl: boolean, alt: boolean, meta: boolean, key: string, func(editor: WikitextEditor): boolean}} KeyShortcut
 * 
 * @typedef {{
 *     getSelection(): number[],
 *     getText(): string
 *     replaceText(from: number, to: number, new_text: string): void
 *     setSelection(from: number, to?: number): void
 * }} WikitextEditor
 */
(function () {
	/**
	 * Reprezentuje edytor wikitekstu w wersji 2010
	 * Obsługuje zarówno tryb bez podświetlania składni, jak i
	 * podświetlanie z użyciem rozszerzenia CodeMirror
	 * 
	 * @class
	 * @param textbox Obiekt jQuery z odwołaniem do pola tekstowego
	 */
	var StandardWikitextEditor = function (textbox) {
		this.textbox = textbox;

		/**
		 * Zwraca położenie końców zaznaczenia.
		 * @returns {number[]}
		 */
		this.getSelection = function () {
			return this.textbox.textSelection('getCaretPosition', { startAndEnd: true });
		};

		/**
		 * Zwraca zawartość edytora.
		 * @returns {string}
		 */
		this.getText = function () {
			return this.textbox.val();
		};

		/**
		 * Zamienia tekst w podanym zakresie na nowy.
		 * 
		 * @param {number} from Początek zakresu, który zamienić
		 * @param {number} to Koniec zakresu, który zamienić
		 * @param {string} new_text Tekst do wstawienia
		 * @returns {void}
		 */
		this.replaceText = function (from, to, new_text) {
			// Zachowaj stare zaznaczenie, aby je potem przywrócić
			var old_sel = this.getSelection();

			this.setSelection(from, to);
			this.insertText(new_text);

			this.setSelection(old_sel[0], old_sel[1]);
		};

		/**
		 * Zaznacza fragment tekstu.
		 * 
		 * @param {number} from Początek zakresu, który należy zaznaczyć
		 * @param {number|undefined} to Koniec zakresu, który należy zaznaczyć. Domyślnie równe wartości `from`
		 * @returns {void}
		 */
		this.setSelection = function (from, to) {
			this.textbox.textSelection('setSelection', {start: from, end: to || from});
		};

		/**
		 * Wstawia tekst do dokumentu
		 * 
		 * @private
		 * @param {string} new_text Tekst do wstawienia
		 */
		this.insertText = function (new_text) {
			// Sprawdź, czy kolorowanie składni jest włączone. Jeśli jest, to w dokumencie istnieje
			// element z klasą .CodeMirror. Rozróżnienie ma na celu dobranie metody wstawiania napisu.
			// 
			// Gołe <textarea> nie obsługuje cofania za pomocą Ctrl+Z, jeśli się zastosuje .textSelection,
			// dlatego używam document.execCommand. Jednak to drugie nie współpracuje najlepiej z CodeMirror,
			// ponieważ nie pozwala przesunąć kursora po wstawieniu tekstu. Jednocześnie, CodeMirror implementuje
			// cofanie zmian, dlatego jeśli użytkownik ma włączone kolorowanie składni, pomiń execCommand.
			var is_code_mirror_enabled = document.getElementsByClassName('CodeMirror').length == 1;

			if(!is_code_mirror_enabled && document.execCommand('insertText', false, new_text)) return;
			this.textbox.textSelection('replaceSelection', new_text);
		};
	};

	/**
	 * Reprezentuje edytor wikitekstu w wersji 2017 (część VE)
	 * 
	 * @class
	 */
	var WikitextEditor2017 = function () {
		/**
		 * Zwraca położenie końców zaznaczenia.
		 * @returns {number[]}
		 */
		this.getSelection = function () {
			var surfaceModel = ve.init.target.getSurface().getModel();
			var range = surfaceModel.getFragment().getSelection().getRange();
			return [range.start, range.end];
		};

		/**
		 * Zwraca zawartość edytora.
		 * @returns {string}
		 */
		this.getText = function () {
			var surfaceModel = ve.init.target.getSurface().getModel();
			var selection = surfaceModel.getFragment().expandLinearSelection('root');
			return selection.getText(true);
		};

		/**
		 * Zamienia tekst w podanym zakresie na nowy.
		 * 
		 * @param {number} from Początek zakresu, który zamienić
		 * @param {number} to Koniec zakresu, który zamienić
		 * @param {string} new_text Tekst do wstawienia
		 * @returns {void}
		 */
		this.replaceText = function (from, to, new_text) {
			var fragment = this.getFragment(from, to);
			fragment.insertContent(new_text);
		};

		/**
		 * Zaznacza fragment tekstu.
		 * 
		 * @param {number} from Początek zakresu, który należy zaznaczyć
		 * @param {number|undefined} to Koniec zakresu, który należy zaznaczyć. Domyślnie równe wartości `from`
		 * @returns {void}
		 */
		this.setSelection = function (from, to) {
			var fragment = this.getFragment(from, to);
			fragment.select();
		};

		/**
		 * Zwraca fragment dokumentu zawarty pomiędzy indeksami
		 * 
		 * @private
		 * @param {number} from Indeks początkowy
		 * @param {number|undefined} to Indeks końcowy. Domyślnie równe wartości `from`
		 * @returns {ve.dm.SurfaceFragment}
		 */
		this.getFragment = function (from, to) {
			var surfaceModel = ve.init.target.getSurface().getModel();
			var selection = surfaceModel.getFragment().expandLinearSelection('root');
			return selection.collapseToStart().adjustLinearSelection(from, to || from);
		};
	};

	/**
	 * Zamienia zaznaczenie na <ref name="" /> i ustawia kursor w cudzysłowach
	 * @param {WikitextEditor} editor - Edytor kodu
	 * @returns {boolean} Czy zamiana się udała
	 */
	var insertRef = function (editor) {
		var selection = editor.getSelection();
		editor.replaceText(selection[0], selection[1], '<ref name="" />');

		editor.setSelection(selection[0] + 11); // +11 umieszcza kursor między cudzysłowami
		return true; // Zawsze się udaje
	};

	/**
	 * Jeżeli kursor (lub początek zaznaczenia) jest wewnątrz szablonu {{r}}, zamienia go na <ref name="" />.
	 * Zwraca informację, czy zamiana się powiodła
	 * @param {WikitextEditor} editor - Edytor kodu
	 * @returns {boolean} Czy zamiana się udała
	 */
	var replaceExistingR = function (editor) {
		var target_template = 'r';

		var text = editor.getText();
		var caret_position = editor.getSelection()[0];

		// Znajdź szablon, w którym aktualnie znajduje się kursor
		// Przesunięcie offsetu o -1 uwzględnia sytuację, gdy kursor jest między klamrami,
		// ale nie na zwenątrz nich
		var template_start = text.lastIndexOf('{{', caret_position - 1);
		if(template_start == -1) return false;
		var template_end = text.indexOf('}}', caret_position - 1);
		if(template_end == -1) return false;
		template_end += 2; // Przesuwa wskaźnik za }}

		// Wyciągnij z tekstu cały szablon; sprawdź, czy nie wyciągnięto za dużo
		// Zakładam, że szablon {{r}} nie zawiera w sobie }}
		var template = text.substring(template_start, template_end);
		var closing_brackets_pos = template.substr(0, template.length-2).indexOf('}}');
		if(closing_brackets_pos > -1) return false;

		// Wczytaj nazwę szablonu
		var template_name = template.substring(2, template.indexOf('|')).toLowerCase();

		// Sprawdź, czy kursor znajduje się w {{r}}
		if(template_name != target_template) return false;

		// Dzieli wywołanie szablonu według znaku potoku i przygotowuje ciąg <ref>ów
		var ref_names = template.substring(0, template.length - 2).split('|');
		var ref_tags = '';
		for(var i = 1; i < ref_names.length; i++) {
			ref_tags += '<ref name="';
			ref_tags += ref_names[i];
			ref_tags += '" />';
		}

		editor.replaceText(template_start, template_end, ref_tags);
		editor.setSelection(template_start + ref_tags.length);
		return true;
	};

	/**
	 * Funkcja stara się zamienić {{r}} w obecnej pozycji kursora na <ref>.
	 * Jeśli to się nie uda, wstawia znacznik
	 * @param {WikitextEditor} editor - Edytor kodu
	 * @returns {boolean} Czy zamiana się udała
	 */
	var replaceROrInsertRef = function (editor) {
		return replaceExistingR(editor) || insertRef(editor);
	};

	/** Tabela do śledzenia ostatnio wpisywanych znaków
	 * @type {string[]} */
	var r_track_queue = [];

	/**
	 * Śledzi wpisywany tekst. W momencie wstawienia {{r| zamienia je na znacznik
	 * @param {WikitextEditor} editor - Pole tekstowe
	 * @param {string} key - Wciśnięty klawisz
	 * @returns {void}
	 */
	var trackTemplateRWhenWriting = function (editor, key) {
		var target_template = '{{r|'; // Poszukiwany szablon
		var replacement = '<ref name="" />'; // Na co zamienić
		var cursor_offset = 11; // Pozycja kursora w nowym tekście (tutaj: między cudzysłowami)

		key = key.toLowerCase();
		r_track_queue.push(key);
		if(r_track_queue.length > target_template.length)
			r_track_queue.shift();
		
		// Szybsza ścieżka, bez dodatkowej operacji na tablicy
		if(key != '|') return;
		if(r_track_queue.join('') != target_template) return;

		// Zdarzenie keydown wywoływane jest przed zaktualizowaniem zawartości pola tekstowego
		// Dlatego odczekaj chwilę i wtedy pobierz ostatnie trzy znaki przed kursorem
		window.requestAnimationFrame(function () {
			var selection_start = editor.getSelection()[0];

			var text = editor.getText();
			var found_template = text.substr(selection_start - target_template.length, target_template.length);

			// Upewnij się, że znaleziono dobry szablon
			if(found_template.toLowerCase() != target_template) return;

			editor.replaceText(selection_start - target_template.length, selection_start, replacement);
			window.requestAnimationFrame(function () {
				editor.setSelection(selection_start - target_template.length + cursor_offset);
			});
		});
	};

	/**
	 * Funkcja pomocnicza do sprawdzania, czy skróty klawiszowe są takie same
	 * @param {KeyShortcut} key1 Skrót 1
	 * @param {KeyShortcut} key2 Skrót 2
	 * @returns {boolean}
	 */
	var sameKeys = function (key1, key2) {
		if(key1.ctrl != key2.ctrl) return false;
		if(key1.alt != key2.alt) return false;
		if(key1.meta != key2.meta) return false;
		if(key1.key != key2.key) return false;
		return true;
	};

	/** Zwraca konfigurację */
	var getConfig = function () {
		var config = {
			shortcuts: {
				insertRef: { ctrl: true, alt: false, meta: false, key: ';', func: insertRef },
				replaceExistingR: { ctrl: true, alt: false, meta: false, key: '\'', func: replaceExistingR }
			},
			trackInsert: false
		};

		if(window.Msz2001_ref_wstawRef !== undefined) config.shortcuts.insertRef = window.Msz2001_ref_wstawRef;
		if(window.Msz2001_ref_zamienR !== undefined) config.shortcuts.replaceExistingR = window.Msz2001_ref_zamienR;
		if(window.Msz2001_ref_zamienWpisywaneR !== undefined) config.trackInsert = window.Msz2001_ref_zamienWpisywaneR;

		// Funkcje są tutaj powtórzone, aby użytkownik nie mógł zmienić
		config.shortcuts.insertRef.func = insertRef;
		config.shortcuts.replaceExistingR.func = replaceExistingR;

		// Jeżeli wstaw <ref> oraz zamień {{r}} mają ten sam skrót,
		// użyj funkcji pomocniczej i wykonuj ją raz
		if(sameKeys(config.shortcuts.insertRef, config.shortcuts.replaceExistingR)) {
			config.shortcuts.insertRef.func = replaceROrInsertRef;
			config.shortcuts.replaceExistingR.func = function (editor) { return true; };
		}

		return config;
	};

	/**
	 * Sprawdza, czy na obecnej stronie jest aktywny VE w trybie wikikodu
	 * 
	 * @returns {boolean}
	 */
	var isVEInWikitextMode = function() {
		if(!window.ve || !ve.init || !ve.init.target) return false;		// Upewnij się, że VE w ogóle istnieje
		if(!ve.init.target.active) return false;		// Sprawdź, czy VE jest aktywny
		if(ve.init.target.getSurface().getMode() !== 'source') return false;	// Sprawdź, czy VE jest w trybie wikikodu
		return true;
	}

	/**
	 * Sprawdza, czy strona, na której przebywa użytkownik jest stroną edycji.
	 * 
	 * @returns {boolean}
	 */
	var isEditPage = function () {
		return ['edit', 'submit'].indexOf(mw.config.get('wgAction')) !== -1;
	};

	/**
	 * Sprawdza, czy strona jest w trybie edycji (w jakimkolwiek edytorze).
	 * 
	 * @returns {boolean}
	 */
	var isEditMode = function () {
		return isEditPage() || isVEInWikitextMode();
	}

	/**
	 * Zwraca edytor odpowiadający aktualnie wyświetlanej stronie
	 * 
	 * @returns {WikitextEditor}
	 */
	var getEditor = function () {
		// Jeżeli VE jest w trybie edycji wikikodu, zwróć odpowiedni adapter
		if(isVEInWikitextMode()){
			return new WikitextEditor2017();
		}

		// Adapter dla edytora wikikodu 2010 (domyślnego)
		return new StandardWikitextEditor($('#wpTextbox1, #ed textarea:focus'));
	};

	/**
	 * Funkcja do obsługi naciśnięć klawiszy.
	 * 
	 * @param {{ ctrlKey: boolean; altKey: boolean; metaKey: boolean; key: string; }} e Dane zdarzenia
	 */
	var handleKeyDown = function (e) {
		var editor = getEditor();
		var config = getConfig();

		// Wywołaj funkcję przyporządkowaną klawiszowi
		Object.entries(config.shortcuts).forEach(function (shortcut) {
			var command = shortcut[1];
			if(!command.key) return;

			if(e.ctrlKey == !!command.ctrl
				&& e.altKey == !!command.alt
				&& e.metaKey == !!command.meta
				&& e.key == command.key) command.func(editor);
		});

		// Śledź wciskane klawisze, tylko wtedy kiedy nie są skrótem klawiszowym
		if(!e.ctrlKey && !e.altKey && !e.metaKey && e.key != 'Shift') {
			if(config.trackInsert) trackTemplateRWhenWriting(editor, e.key);
		}
	}

	/**
	 * Inicjalizuje działanie narzędzia. Nasłuch klawiszy jest dodawany zawsze,
	 * ale procedura obsługi wywołuje się tylko kiedy użytkownik edytuje wikikod.
	 * 
	 * @returns {void}
	 */
	var initialize = function () {
		// Zaczekaj aż biblioteki się załadują
		$.when(
			$.ready,
			mw.loader.using('jquery.textSelection')
		).done(function () {
			// Dodaj nasłuchiwanie naciśnięć klawiszy
			// Przekazuj je dalej tylko w trybie edycji
			$(document).on('keydown', function (e) {
				if(!isEditMode()) return;
				handleKeyDown(e);
			});
		});
	}

	initialize();
})();

// </nowiki>

// Przykładowa konfiguracja u siebie:
//
// Ctrl+Q wstawi przypis:
// window.Msz2001_ref_wstawRef = { ctrl: true, alt: false, meta: false, key: 'q' };
// Ctrl+Shift+Q zamieni szablon R na znacznik:
// window.Msz2001_ref_zamienR = { ctrl: true, alt: false, meta: false, key: 'Q' };
// Po wpisaniu szablonu, automatycznie zostanie wstawiony znacznik
// window.Msz2001_ref_zamienWpisywaneR = true;