MediaWiki:Common.js

From DreamWorks School of Dragons Wiki
Jump to navigation Jump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/*Portal*/
mw.loader.using( ['jquery.ui.tabs'], function() {
    $(function() {
        var $tabs 
= $("#portal_slider").tabs({ fx: {opacity:'toggle', duration:100} } );
        $("[class^=portal_sliderlink]").click(function() { // bind click event to link
            $tabs.tabs('select', this.className.replace("portal_sliderlink_", ""));
            setTimeout(function() {
    $(window).scroll();
}, 1000);
            return false;
        });
    });
});


mw.hook('wikipage.content').add(function($content){
    logToConsole("contentHook triggered.");
    ColorGuide.initialize();
});



function logToConsole(message){
    if(window.console && window.console.log){
        console.log(message);
    }
}


// Color Guide
//-------------
(function (ColorGuide) {
    // constants for ColorGuide element IDs
    /** @constant */ var CLASS_PREVIEW_CANVAS = "colorPreviewCanvasHolder";
    /** @constant */ var CLASS_PRIMARY_PICKER = "primaryColorPickerHolder";
    /** @constant */ var CLASS_SECONDARY_PICKER = "secondaryColorPickerHolder";
    /** @constant */ var CLASS_TERTIARY_PICKER = "tertiaryColorPickerHolder";
    /** @constant */ var CLASS_BACKGROUND_PICKER = "backgroundColorPickerHolder";
    /** @constant */ var CLASS_APPLY_BUTTON = "colorGuideApplyColorButtonHolder";

    // constants for allowed dataset attributes on ColorGuide elements
    // for example 'MyThing' would resolve to the attribute 'data-my-thing' in HTML
    // for more information on 'dataset', see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset
    /** @constant
     * passes a style to the generated child element */
    var DATASET_STYLE = "style";
    /** @constant
     * wikiFile pointing to the rendered detailTexture */
    var DATASET_DETAIL_IMAGE = "detailImgSrc";
    /** @constant
     * wikiFile pointing to the rendered colorTexture*/
    var DATASET_COLOR_IMAGE = 'colorImgSrc';
    /** @constant
     * preferred width of the canvas in pixels ('123') or fraction of screen width ('123%') */
    var DATASET_CANVAS_WIDTH = "canvasWidth";
    /** @constant
     * maximum width of the canvas in pixels ('123') or fraction of screen width ('123%') */
    var DATASET_CANVAS_MAX_WIDTH = "canvasMaxWidth";
    /** @constant
     * set an initial value for the color pickers ('#RRGGBB')*/
    var DATASET_DEFAULT_VALUE = "defaultValue";
    /** @constant
     * assign element to a group if multiple ColorGuides are on one page */
    var DATASET_GROUP = "group";

    // constant lists of element classes that make up a valid ColorGuide instance
    /** @constant
     * @type {string[]} */
    var INSTANCE_REQUIRED_ELEMENTS = [CLASS_PREVIEW_CANVAS, CLASS_PRIMARY_PICKER, CLASS_SECONDARY_PICKER, CLASS_TERTIARY_PICKER, CLASS_APPLY_BUTTON];
    /** @constant
     * @type {string[]} */
    var INSTANCE_OPTIONAL_ELEMENTS = [CLASS_BACKGROUND_PICKER];

    // constant map of ColorGuide elements and their allowed attributes
    /** @typedef {{requiredDataSets: string[], optionalDataSets: string[]}} AllowedDataSets */
    /** @constant
     *  @type {Object.<string, AllowedDataSets>} */
    var INSTANCE_ELEMENT_ATTRIBUTES = (function () {
        /** @type {Object.<string, AllowedDataSets>} */
        var result = {};
        result[CLASS_PREVIEW_CANVAS] = {
            requiredDataSets: [DATASET_DETAIL_IMAGE, DATASET_COLOR_IMAGE, DATASET_CANVAS_WIDTH],
            optionalDataSets: [DATASET_CANVAS_MAX_WIDTH, DATASET_STYLE, DATASET_GROUP],
        };
        result[CLASS_PRIMARY_PICKER] = {
            requiredDataSets: [],
            optionalDataSets: [DATASET_DEFAULT_VALUE, DATASET_STYLE, DATASET_GROUP],
        };
        result[CLASS_SECONDARY_PICKER] = {
            requiredDataSets: [],
            optionalDataSets: [DATASET_DEFAULT_VALUE, DATASET_STYLE, DATASET_GROUP],
        };
        result[CLASS_TERTIARY_PICKER] = {
            requiredDataSets: [],
            optionalDataSets: [DATASET_DEFAULT_VALUE, DATASET_STYLE, DATASET_GROUP],
        };
        result[CLASS_BACKGROUND_PICKER] = {
            requiredDataSets: [],
            optionalDataSets: [DATASET_DEFAULT_VALUE, DATASET_STYLE, DATASET_GROUP],
        };
        result[CLASS_APPLY_BUTTON] = {
            requiredDataSets: [],
            optionalDataSets: [DATASET_STYLE, DATASET_GROUP],
        };
        return result;
    })();

    /**
     * @param expectedClasses: {string[]} list of expected classes
     * @param foundClasses: {string[]} list of found classes
     * @returns string[] list of missing classes, or empty list
     */
    function findMissingClasses(expectedClasses, foundClasses) {
        var missingClasses = [];
        var i;
        for (i = 0; i < expectedClasses.length; i++) {
            missingClasses.push(expectedClasses[i]);
        }
        for (i = 0; i < foundClasses.length; i++) {
            var foundClass = foundClasses[i];
            var foundIndex = missingClasses.indexOf(foundClass);
            if (foundIndex >= 0) {
                missingClasses.splice(foundIndex, 1);
            }
        }
        return missingClasses;
    }

    /**
     * @returns {Object.<string, (HTMLElement || null)[]>} valid groups by groupName, containing the [requiredElements..., optionalElements...] in order
     */
    function validateInstances() {
        /** @type {string[]} */
        var groupList = [];
        /** @type {Object.<string, HTMLElement[]>} */
        var groupElements = {};
        /** @type {Object.<string, string[]>} */
        var groupElementClasses = {};

        var i, group, elementIdx, className;
        for (i = 0; i < INSTANCE_REQUIRED_ELEMENTS.length; i++) {
            className = INSTANCE_REQUIRED_ELEMENTS[i];
            var classElements = document.getElementsByClassName(className);
            for (elementIdx = 0; elementIdx < classElements.length; elementIdx++) {
                /** @type {HTMLElement} */
                var element = classElements.item(elementIdx);
                group = element.dataset[DATASET_GROUP] || "__null";

                groupList.push(group);

                var arr = groupElements[group] || [];
                arr.push(element);
                groupElements[group] = arr;

                var arr2 = groupElementClasses[group] || [];
                arr2.push(className);
                groupElementClasses[group] = arr2;
            }
        }

        /** @type {Object.<string, (HTMLElement || null)[]>} */
        var validInstances = {};

        for (i = 0; i < groupList.length; i++) {
            group = groupList[i];
            var missingClasses = findMissingClasses(INSTANCE_REQUIRED_ELEMENTS, groupElementClasses[group]);
            if (missingClasses.length > 0) {
                logToConsole("Error: ColorGuide group '" + group + "' was missing elements: " + JSON.stringify(missingClasses));
            } else {
                validInstances[group] = groupElements[group];
            }
        }

        // for every valid group, append optional elements || null
        var validGroupList = Object.keys(validInstances);
        for (var optionalIdx = 0; optionalIdx < INSTANCE_OPTIONAL_ELEMENTS.length; optionalIdx++) {
            /** @type {Object.<string, HTMLElement>} */
            var foundOptionals = {};
            var optionalClassName = INSTANCE_OPTIONAL_ELEMENTS[optionalIdx];
            var optionalClassElements = document.getElementsByClassName(optionalClassName);
            for (elementIdx = 0; elementIdx < optionalClassElements.length; elementIdx++) {
                /** @type {HTMLElement} */
                var optionalElement = optionalClassElements[elementIdx];
                group = optionalElement.dataset[DATASET_GROUP] || "__null";

                if (validGroupList.indexOf(group) < 0) {
                    logToConsole("Error: ColorGuide element '" + optionalClassName + "' has invalid group: '" + group + "'");
                    continue;
                }

                foundOptionals[group] = optionalElement;
            }

            for (i = 0; i < validGroupList.length; i++) {
                group = validGroupList[i];
                validInstances[group].push(foundOptionals[group] || null);
            }
        }

        // verify required dataSet attributes
        var classSequence = INSTANCE_REQUIRED_ELEMENTS.concat(INSTANCE_OPTIONAL_ELEMENTS);
        for (var j = 0; j < validGroupList.length; j++) {
            group = validGroupList[j];
            var instanceElements = validInstances[group];
            var validDataSet = true;
            for (i = 0; i < classSequence.length; i++) {
                className = classSequence[i];
                var instanceElement = instanceElements[i];
                var requiredDataSets = INSTANCE_ELEMENT_ATTRIBUTES[className].requiredDataSets;
                for (var dataSetIdx = 0; dataSetIdx < requiredDataSets.length; dataSetIdx++) {
                    var requiredDataSet = requiredDataSets[dataSetIdx];
                    if (!(requiredDataSet in instanceElement.dataset)) {
                        logToConsole("Error: ColorGuide element '" + className + "' of group '" + group
                            + "' is missing dataSet attribute '" + requiredDataSet + "'");
                        validDataSet = false;
                        break;
                    }
                }

                if (!validDataSet) {
                    break;
                }
            }

            if (!validDataSet) {
                delete validInstances[group];
            }
        }

        return validInstances;
    }

    /** @typedef {function():number} canvasWidthProvider */

    /** @typedef {{
     *      previewCanvas: HTMLCanvasElement,
     *      widthProvider: canvasWidthProvider,
     *      detailCanvas: HTMLCanvasElement,
     *      detailImage: HTMLImageElement,
     *      colorCanvas: HTMLCanvasElement,
     *      colorImage: HTMLImageElement,
     *      primaryPicker: HTMLInputElement,
     *      secondaryPicker: HTMLInputElement,
     *      tertiaryPicker: HTMLInputElement,
     *      backgroundPicker: HTMLInputElement | null
     * }} ColorGuideInstance */

    /**
     * @param {string | null} text
     * @returns {{pixels: (number | null), percentage: (number | null)}}
     */
    function parseSizeText(text) {
        /** @type {number | null} */
        var pixelsValue = null;
        /** @type {number | null} */
        var percentageValue = null;
        if (text) {
            if (text.charAt(text.length - 1) === '%') {
                percentageValue = parseInt(text.slice(0, text.length - 1));
            } else {
                pixelsValue = parseInt(text);
            }
        }
        return {pixels: pixelsValue, percentage: percentageValue};
    }

    /**
     * uses the mediaWiki api to acquire an object containing the url of an image-file from the wiki
     * @param {*} imageName fileName of the image to find
     * @param {*} callback function to be called with the queried json object
     */
    function queryImageSource(imageName, callback) {
        var api = new mw.Api();
        api.get({
            action: 'query',
            titles: imageName,
            prop: 'imageinfo',
            iiprop: 'url',
            format: 'json'
        })
            .done(callback)
            .fail(function (jqXHR, textStatus, errorThrown) {
                logToConsole("query failed, error: " + errorThrown);
            });
    }

    /**
     * acquires the image url from a (json) data-object obtained by using `queryImageSource`
     * @param {*} data json data object containing the path `query.pages.{pageid}.imageinfo[0].url`
     * @returns url of image or null if path not found
     */
    function getImageSource(data) {
        if (!data.hasOwnProperty('query') || !data.query.hasOwnProperty('pages') || Object.keys(data.query.pages).length <= 0) {
            return null;
        }

        var pageKeys = Object.keys(data.query.pages);
        if (pageKeys.length <= 0) {
            return null;
        }

        //we only expect one page, so just get whatever the first page happens to be
        var page = data.query.pages[pageKeys[0]];
        if (!page.hasOwnProperty('imageinfo') || page.imageinfo.length <= 0 || !page.imageinfo[0].hasOwnProperty('url')) {
            return null;
        }

        return page.imageinfo[0].url;
    }

    /**
     * @param container: {HTMLElement}
     * @param dataSetAttribute: {string}
     * @param onLoadCallback: {}
     * @returns {HTMLImageElement}
     */
    function addImageHolder(container, dataSetAttribute, onLoadCallback) {
        var image = new Image();
        image.onload = onLoadCallback;
        image.crossOrigin = "anonymous";
        image.style.cssText = "display:none";

        var imageSourceText = container.dataset[dataSetAttribute];
        queryImageSource(imageSourceText, function (data) {
            if (data === null) {
                logToConsole("image query callback data was undefined or null!");
                return;
            }

            var imageSource = getImageSource(data);
            if (imageSource === null) {
                logToConsole("unable to find imageSource for file '" + imageSourceText + "' in query result! (probably invalid fileName specified)");
                return;
            }

            image.src = imageSource;
            container.appendChild(image);
        });

        return image;
    }

    /**
     * @param container: {HTMLElement | null}
     * @returns {HTMLInputElement | null}
     */
    function addColorPicker(container) {
        if (container === null) {
            return null;
        }

        //<input type="color" value="value">
        var picker = document.createElement("input");
        picker.type = "color";
        picker.value = container.dataset[DATASET_DEFAULT_VALUE] || "#000000";
        picker.style.cssText = container.dataset[DATASET_STYLE];
        container.appendChild(picker);
        return picker;
    }

    /**
     * @param group: {string} group-name
     * @param elements: {HTMLElement[]} elements in order of [requiredElements..., optionalElements...]
     * @returns {ColorGuideInstance}
     */
    function initializeColorGuideInstance(group, elements) {
        var classSequence = INSTANCE_REQUIRED_ELEMENTS.concat(INSTANCE_OPTIONAL_ELEMENTS);

        function getElement(elementClass) {
            return elements[classSequence.indexOf(elementClass)];
        }

        var canvasDiv = getElement(CLASS_PREVIEW_CANVAS);
        var targetWidth = parseSizeText(canvasDiv.dataset[DATASET_CANVAS_WIDTH] || "640");
        var maxWidth = parseSizeText(canvasDiv.dataset[DATASET_CANVAS_MAX_WIDTH] || null);

        var previewCanvas = document.createElement("CANVAS");
        previewCanvas.style.cssText = canvasDiv.dataset[DATASET_STYLE] || "";
        canvasDiv.appendChild(previewCanvas);

        var applyButtonDiv = getElement(CLASS_APPLY_BUTTON);
        var applyButton = document.createElement("input");
        applyButton.type = "button";
        applyButton.value = "Apply Colors";
        applyButton.style.cssText = applyButtonDiv.dataset[DATASET_STYLE];
        applyButton.onclick = function () {ColorGuide.applyColors(group);};
        applyButtonDiv.appendChild(applyButton);

        /** @type {ColorGuideInstance} */
        var instance = {};
        instance.previewCanvas = previewCanvas;
        instance.widthProvider = function widthProvider() {
            var targetPixels = targetWidth.pixels != null ? targetWidth.pixels : (window.innerWidth * targetWidth.percentage / 100);
            if (maxWidth.pixels === null && maxWidth.percentage === null) {
                return targetPixels;
            }

            var maxPixels = maxWidth.pixels != null ? maxWidth.pixels : (window.innerWidth * maxWidth.percentage / 100);
            return Math.floor(Math.min(targetPixels, maxPixels));
        };

        instance.detailCanvas = document.createElement("CANVAS");
        instance.colorCanvas = document.createElement("CANVAS");

        var imagesToLoad = 2;
        var onImageLoadCallback = function () {
            imagesToLoad--;
            if (imagesToLoad === 0) {
                ColorGuide.applyColors(group);
            }
        };
        instance.detailImage = addImageHolder(canvasDiv, DATASET_DETAIL_IMAGE, onImageLoadCallback);
        instance.colorImage = addImageHolder(canvasDiv, DATASET_COLOR_IMAGE, onImageLoadCallback);

        instance.primaryPicker = addColorPicker(getElement(CLASS_PRIMARY_PICKER));
        instance.secondaryPicker = addColorPicker(getElement(CLASS_SECONDARY_PICKER));
        instance.tertiaryPicker = addColorPicker(getElement(CLASS_TERTIARY_PICKER));
        instance.backgroundPicker = addColorPicker(getElement(CLASS_BACKGROUND_PICKER));

        return instance;
    }

    /**
     * @returns {Object.<string, ColorGuideInstance>}
     */
    function initializeInstances() {
        var result = {};

        var validInstances = validateInstances();
        var groupList = Object.keys(validInstances);
        for (var i = 0; i < groupList.length; i++) {
            var group = groupList[i];
            result[group] = initializeColorGuideInstance(group, validInstances[group]);
        }

        return result;
    }

    /** @type {Object.<string, ColorGuideInstance>} */
    ColorGuide.instancesByGroup = null;

    ColorGuide.initialize = function initialize() {
        if (ColorGuide.instancesByGroup === null) {
            ColorGuide.instancesByGroup = initializeInstances();
        }
    };

    ColorGuide.applyColors = function applyColors(group) {
        var instance = ColorGuide.instancesByGroup[group];
        if (instance.detailImage.width !== instance.colorImage.width || instance.detailImage.height !== instance.colorImage.height) {
            logToConsole("detailImage and colorImage for group '" + group + "' have different dimensions!");
            return;
        }

        var targetWidth = instance.widthProvider();
        var targetHeight = getTargetHeight(instance.detailImage, targetWidth);
        instance.previewCanvas.width = targetWidth;
        instance.previewCanvas.height = targetHeight;
        loadImageToCanvas(instance.detailImage, instance.detailCanvas, targetWidth, targetHeight);
        loadImageToCanvas(instance.colorImage, instance.colorCanvas, targetWidth, targetHeight);

        if (!allDimensionsMatching({width: targetWidth, height: targetHeight}, instance.detailCanvas, instance.colorCanvas, instance.previewCanvas)) {
            logToConsole("mismatching dimensions of detailCanvas, colorCanvas and previewCanvas | group: '" + group + "'");
            return;
        }

        var color1 = hexToRgb(instance.primaryPicker.value);
        var color2 = hexToRgb(instance.secondaryPicker.value);
        var color3 = hexToRgb(instance.tertiaryPicker.value);

        if (color1 == null || color2 == null || color3 == null) {
            logToConsole("unable to parse color input for group '" + group + "'");
            return;
        }

        if (instance.backgroundPicker != null) {
            instance.previewCanvas.style.backgroundColor = instance.backgroundPicker.value;
        } else {
            instance.previewCanvas.style.backgroundColor = "transparent";
        }

        buildResultCanvas(group, instance, color1, color2, color3);
    };

    /**
     * builds the displayCanvas given a loaded detail- and color-canvas, along with the target primary, secondary and tertiary colors
     * @param group: {string}
     * @param instance: {ColorGuideInstance}
     * @param primaryColor: {r: number, g: number, b: number}
     * @param secondaryColor: {r: number, g: number, b: number}
     * @param tertiaryColor: {r: number, g: number, b: number}
     */
    function buildResultCanvas(group, instance, primaryColor, secondaryColor, tertiaryColor) {
        var width = instance.previewCanvas.width;
        var height = instance.previewCanvas.height;

        var detailData = instance.detailCanvas.getContext("2d").getImageData(0, 0, width, height);
        var colorData = instance.colorCanvas.getContext("2d").getImageData(0, 0, width, height);
        var displayData = new ImageData(width, height);

        var redFactor, greenFactor, blueFactor, blackFactor, targetRed, targetGreen, targetBlue, detailRed, detailGreen, detailBlue;
        for (var i = 0; i < detailData.data.length; i += 4) {
            redFactor = colorData.data[i] / 255;
            greenFactor = colorData.data[i + 1] / 255;
            blueFactor = colorData.data[i + 2] / 255;
            blackFactor = 1.0 - Math.min(redFactor + greenFactor + blueFactor, 1.0);

            targetRed = blackFactor * primaryColor.r + blueFactor * secondaryColor.r + greenFactor * tertiaryColor.r;
            targetGreen = blackFactor * primaryColor.g + blueFactor * secondaryColor.g + greenFactor * tertiaryColor.g;
            targetBlue = blackFactor * primaryColor.b + blueFactor * secondaryColor.b + greenFactor * tertiaryColor.b;

            detailRed = detailData.data[i];
            detailGreen = detailData.data[i + 1];
            detailBlue = detailData.data[i + 2];

            displayData.data[i] = detailRed * lerp(targetRed, 255, redFactor) / 255;
            displayData.data[i + 1] = detailGreen * lerp(targetGreen, 255, redFactor) / 255;
            displayData.data[i + 2] = detailBlue * lerp(targetBlue, 255, redFactor) / 255;
            displayData.data[i + 3] = detailData.data[i + 3];
        }

        instance.previewCanvas.getContext("2d").putImageData(displayData, 0, 0);
    }

    /**
     * validates that all passed canvases have the same dimensions as targetDimensions
     * @param targetDimensions: {width: {number}, height: {number}}
     * @param arguments: {HTMLCanvasElement...} variable amount of canvases to be checked
     * @returns boolean value, true when all Dimensions match, false when they don't
     */
    function allDimensionsMatching(targetDimensions) {
        for (var i = 1; i < arguments.length; i++) {
            if (arguments[i].width !== targetDimensions.width) {
                return false;
            }
            if (arguments[i].height !== targetDimensions.height) {
                return false;
            }
        }
        return true;
    }

    /**
     * works just like unity's lerp (minus the clamping)
     * @param {*} a startValue (mapped to t = 0)
     * @param {*} b endValue (mapped to t = 1)
     * @param {*} t
     * @returns the value of f(t) given f(0) = a and f(1) = b, where f is a linear function
     */
    function lerp(a, b, t) {
        return a + ((b - a) * t);
    }

    /**
     * takes a hex color (#rrggbb) and turns it into an rgb object
     * @param {string} hex
     * @returns {{r: number, g: number, b: number} | null} if the hex color was invalid or an object containing keys{'r', 'g', 'b'}
     */
    function hexToRgb(hex) {
        var result = /^#?([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.exec(hex);
        return result ? {
            r: parseInt(result[1], 16),
            g: parseInt(result[2], 16),
            b: parseInt(result[3], 16)
        } : null;
    }

    /**
     * @param image: {number}
     * @param targetWidth: {number}
     * @returns {number}
     */
    function getTargetHeight(image, targetWidth) {
        return Math.floor(targetWidth / image.width * image.height);
    }

    /**
     * loads an image to the canvas and rescales both to the targetDimensions
     * @param image: {HTMLImageElement}
     * @param canvas: {HTMLCanvasElement}
     * @param targetWidth: {number}
     * @param targetHeight: {number}
     */
    function loadImageToCanvas(image, canvas, targetWidth, targetHeight) {
        canvas.width = targetWidth;
        canvas.height = targetHeight;
        var context = canvas.getContext("2d");
        context.drawImage(image, 0, 0, targetWidth, targetHeight);
    }

})(window.ColorGuide = window.ColorGuide || {});

/* UploadMultipleFiles */
mw.config.set('UMFBypassLicenseCheck',true);


/**
 * Make sidebar sections collapsible
 */
$(function(){
	$panel = $('#mw-panel');
	$("#mw-panel .portal").each(function(index, el){
		var $el = $(el);
		var $id = $el.attr("id");
		if(!$id){
			return;
		}
		// for < 1366px
		$el.removeClass('expanded');
		// for >= 1366px
		if(localStorage.getItem('sidebar_c_'+$id) === "y"){
			$el.addClass('collapsed').find('.body').slideUp(0);
		}
	});
	$("#mw-panel .portal").on("click", "h3", function(event){
		var $el = $(this).parent();
		var $id = $el.attr("id");
		if(!$id){
			return;
		}
		event.stopPropagation();
		if($panel.width() < 200){
			$el.toggleClass('collapsed');
			if($el.hasClass('collapsed')){ // more consistent between class and slide status.
				localStorage.setItem('sidebar_c_'+$id, "y");
				$el.find('.body').slideUp('fast');
			}
			else{
				localStorage.setItem('sidebar_c_'+$id, "n");
				$el.find('.body').slideDown('fast');
			}
		}
		else{
			$("#mw-panel .portal").not($el).removeClass('expanded');
			$el.toggleClass('expanded');
		}
	});
});