MediaWiki:Gadget-ref-klawiatura.js
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;