Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.

  • Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
  • Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
  • Internet Explorer/Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
  • Opera: Strg+F5
//Dokumentation unter [[Benutzer:Schnark/js/veSuggestions]] <nowiki>
/*global mediaWiki, OO, ve, unicodeJS*/
(function ($, mw) {
"use strict";

//split a text into words
function getWords (text) {
	var ujsString = new unicodeJS.TextString(text),
		pos, prevPos, len = text.length,
		words = [];
	for (pos = 0; pos <= len; pos++) {
		if (unicodeJS.wordbreak.isBreak(ujsString, pos)) {
			if (prevPos !== undefined) {
				words.push(text.slice(prevPos, pos));
			}
			prevPos = pos;
		}
	}
	return words;
}

function CompletionSuggestionWidget (surface, options) {
	this.surface = surface;
	this.surfaceModel = this.surface.getModel();
	this.$documentElement = this.surface.getView().getDocument().getDocumentNode().$element;
	this.options = $.extend({
		gap: 5, //how many pixels below the cursor to show the suggestions
		words: [], //words to suggest
		minWordLength: 4, //minimal length of a word to be added to suggestions
		minFragmentLength: 3, //minimal length before suggestions are shown
		maxSuggestions: 5, //maximal number of suggestions shown
		delay: 20 //how long to delay before updating suggestions (in ms)
	}, options);
	this.$documentElement.on({
		keydown: this.onDocumentKeyDown.bind(this),
		keypress: this.onDocumentKeyPress.bind(this),
		keyup: this.onDocumentKeyUp.bind(this),
		click: this.onDocumentClick.bind(this),
		blur: this.onDocumentBlur.bind(this)
	});
	this.isVisible = false;
	this.currentSuggestions = [];
	this.currentSuggestionIndex = 0;
	this.currentFragmentLength = 0;
	this.$element = $('<div>').on('click', this.onClick.bind(this))
		.css({position: 'absolute', zIndex: 100}).appendTo('body');
}

CompletionSuggestionWidget.prototype.hide = function () {
	if (this.isVisible) {
		this.isVisible = false;
		this.$element.hide();
	}
};

CompletionSuggestionWidget.prototype.updateDisplay = function () {
	var boundingRect = this.surface.getView().getSelection().getSelectionBoundingRect(),
		offset = this.surface.$element.offset(),
		selectedIndex = this.currentSuggestionIndex,
		css = {};
	css.top = boundingRect.bottom + this.options.gap + offset.top;
	if (this.$documentElement.css('direction') === 'rtl') {
		css.right = $(document).width() - boundingRect.left - offset.left;
	} else {
		css.left = boundingRect.left + offset.left;
	}
	this.$element.css(css);
	//size=1 would create a dropdown, so use Math.max(2, length)
	this.$element.html(mw.html.element('select', {size: Math.max(2, this.currentSuggestions.length)}, new mw.html.Raw(
		this.currentSuggestions.map(function (word, i) {
			return mw.html.element('option', {selected: i === selectedIndex}, word);
		}).join('')
	)));
	if (!this.isVisible) {
		this.isVisible = true;
		this.$element.show();
	}
};

CompletionSuggestionWidget.prototype.addWordsFromContent = function () {
	this.addWords(getWords(mw.config.get('wgTitle')));
	this.addWords(getWords(this.surfaceModel.getDocument().data.getText(true)));
};

CompletionSuggestionWidget.prototype.addWord = function (word) {
	if (
		word.length >= this.options.minWordLength &&
		this.options.words.indexOf(word) === -1
	) {
		this.options.words.push(word);
	}
};

CompletionSuggestionWidget.prototype.addWords = function (words) {
	var i;
	for (i = 0; i < words.length; i++) {
		this.addWord(words[i]);
	}
};

CompletionSuggestionWidget.prototype.getSuggestionsFor = function (fragment) {
	if (fragment.length < this.options.minFragmentLength) {
		return [];
	}
	var suggestions = this.options.words.filter(function (word) {
		return word.slice(0, fragment.length) === fragment && word !== fragment;
	});
	//TODO sort the suggestions in a sensible way
	if (suggestions.length > this.options.maxSuggestions) {
		suggestions.length = this.options.maxSuggestions;
	}
	return suggestions;
};

CompletionSuggestionWidget.prototype.insertSuggestion = function () {
	var suggestion = this.currentSuggestions[this.currentSuggestionIndex];
	suggestion = suggestion.slice(this.currentFragmentLength);
	this.surfaceModel.getFragment().collapseToEnd()
		.insertContent(suggestion.split(''), true)
		.collapseToEnd().select();
	this.hide();
};

CompletionSuggestionWidget.prototype.changeSelectedSuggestion = function (d) {
	this.currentSuggestionIndex = (
		this.currentSuggestionIndex + d +
		this.currentSuggestions.length) % this.currentSuggestions.length;
	this.updateDisplay();
};

CompletionSuggestionWidget.prototype.showSuggestions = function (fragment) {
	this.currentSuggestions = this.getSuggestionsFor(fragment);
	this.currentSuggestionIndex = 0;
	this.currentFragmentLength = fragment.length;
	if (this.currentSuggestions.length) {
		this.updateDisplay();
	} else {
		this.hide();
	}
};

CompletionSuggestionWidget.prototype.getCursorContext = function () {
	var data, offset, wordRange, selection;

	data = this.surfaceModel.getDocument().data;
	selection = this.surfaceModel.getSelection();
	if (!selection || !selection.getRange) {
		return;
	}
	offset = selection.getRange();
	if (!offset.isCollapsed()) {
		return {};
	}
	offset = offset.end;
	wordRange = data.getWordRange(offset);
	if (wordRange.start === offset) {
		return {
			word: offset ? data.getText(false, data.getWordRange(offset - 1)) : ''
		};
	}
	if (wordRange.end === offset) {
		return {
			word: data.getText(false, wordRange),
			atWordbreak: true
		};
	}
	return {};
};

CompletionSuggestionWidget.prototype.debouncedInput = function () {
	var context = this.getCursorContext();
	if(!context) {
		return;
	}
	if (context.atWordbreak) {
		//we are at the end of a (partial) word, show suggestions
		this.showSuggestions(context.word);
		return;
	} else if (context.word) {
		//user completed a word, add it to the list to suggest it in future
		this.addWord(context.word);
	}
	this.hide();
};

CompletionSuggestionWidget.prototype.onInput = function () {
	if (this.debouncedInputId) {
		this.hide(); //it seems to take longer, so hide the outdated suggestions
		clearTimeout(this.debouncedInputId);
	}
	this.debouncedInputId = setTimeout(function () {
		this.debouncedInputId = false;
		this.debouncedInput();
	}.bind(this), this.options.delay);
};

CompletionSuggestionWidget.prototype.onDocumentKeyDown = function (e) {
	var handled = false;
	if (this.isVisible) {
		switch (e.keyCode) {
		case OO.ui.Keys.UP:
			this.changeSelectedSuggestion(-1);
			handled = true;
			break;
		case OO.ui.Keys.DOWN:
			this.changeSelectedSuggestion(1);
			handled = true;
			break;
		case OO.ui.Keys.ENTER:
			this.insertSuggestion();
			handled = true;
			break;
		case OO.ui.Keys.ESCAPE:
			this.hide();
			handled = true;
			break;
		}
	}
	if (handled) {
		e.preventDefault();
		e.stopPropagation();
		this.preventCurrentKey = true;
	} else {
		this.onInput();
	}
};

CompletionSuggestionWidget.prototype.onDocumentKeyPress = function (e) {
	if (this.preventCurrentKey) {
		e.preventDefault();
		e.stopPropagation();
	}
};

CompletionSuggestionWidget.prototype.onDocumentKeyUp = function (e) {
	if (this.preventCurrentKey) {
		e.preventDefault();
		e.stopPropagation();
		this.preventCurrentKey = false;
	}
};

CompletionSuggestionWidget.prototype.onDocumentClick = function () {
	this.onInput();
};

CompletionSuggestionWidget.prototype.onDocumentBlur = function () {
	window.setTimeout(function () {
		this.hide();
	}.bind(this), 200); //delay to have it still visible when the user clicks on a suggestion
};

CompletionSuggestionWidget.prototype.onClick = function () {
	this.insertSuggestion();
};

mw.hook('ve.activationComplete').add(function () {
	var csWidget = new CompletionSuggestionWidget(ve.init.target.getSurface());
	csWidget.addWordsFromContent();
	//TODO other surfaces as well?
});

})(jQuery, mediaWiki);
//</nowiki>